Separate OpenTelemetry.Api B3 Trace Propagator to a new Nuget Package (#3244)
This commit is contained in:
parent
9e827ece64
commit
5c168a3faf
|
|
@ -226,6 +226,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started-prometheus-
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "customizing-the-sdk", "docs\logs\customizing-the-sdk\customizing-the-sdk.csproj", "{6C7A1595-36D6-4229-BBB5-5A6B5791791D}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "customizing-the-sdk", "docs\logs\customizing-the-sdk\customizing-the-sdk.csproj", "{6C7A1595-36D6-4229-BBB5-5A6B5791791D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Propagators", "src\OpenTelemetry.Extensions.Propagators\OpenTelemetry.Extensions.Propagators.csproj", "{E91B2E40-E428-43B3-8A43-09709F0E69E4}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Propagators.Tests", "test\OpenTelemetry.Extensions.Propagators.Tests\OpenTelemetry.Extensions.Propagators.Tests.csproj", "{476D804B-BFEC-4D34-814C-DFFD97109989}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -472,6 +476,14 @@ Global
|
||||||
{6C7A1595-36D6-4229-BBB5-5A6B5791791D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{6C7A1595-36D6-4229-BBB5-5A6B5791791D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{6C7A1595-36D6-4229-BBB5-5A6B5791791D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{6C7A1595-36D6-4229-BBB5-5A6B5791791D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{6C7A1595-36D6-4229-BBB5-5A6B5791791D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{6C7A1595-36D6-4229-BBB5-5A6B5791791D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E91B2E40-E428-43B3-8A43-09709F0E69E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E91B2E40-E428-43B3-8A43-09709F0E69E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E91B2E40-E428-43B3-8A43-09709F0E69E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E91B2E40-E428-43B3-8A43-09709F0E69E4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{476D804B-BFEC-4D34-814C-DFFD97109989}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{476D804B-BFEC-4D34-814C-DFFD97109989}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{476D804B-BFEC-4D34-814C-DFFD97109989}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{476D804B-BFEC-4D34-814C-DFFD97109989}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
OpenTelemetry.Extensions.Propagators.B3Propagator
|
||||||
|
OpenTelemetry.Extensions.Propagators.B3Propagator.B3Propagator() -> void
|
||||||
|
OpenTelemetry.Extensions.Propagators.B3Propagator.B3Propagator(bool singleHeader) -> void
|
||||||
|
override OpenTelemetry.Extensions.Propagators.B3Propagator.Extract<T>(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func<T, string, System.Collections.Generic.IEnumerable<string>> getter) -> OpenTelemetry.Context.Propagation.PropagationContext
|
||||||
|
override OpenTelemetry.Extensions.Propagators.B3Propagator.Fields.get -> System.Collections.Generic.ISet<string>
|
||||||
|
override OpenTelemetry.Extensions.Propagators.B3Propagator.Inject<T>(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action<T, string, string> setter) -> void
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
OpenTelemetry.Extensions.Propagators.B3Propagator
|
||||||
|
OpenTelemetry.Extensions.Propagators.B3Propagator.B3Propagator() -> void
|
||||||
|
OpenTelemetry.Extensions.Propagators.B3Propagator.B3Propagator(bool singleHeader) -> void
|
||||||
|
override OpenTelemetry.Extensions.Propagators.B3Propagator.Extract<T>(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Func<T, string, System.Collections.Generic.IEnumerable<string>> getter) -> OpenTelemetry.Context.Propagation.PropagationContext
|
||||||
|
override OpenTelemetry.Extensions.Propagators.B3Propagator.Fields.get -> System.Collections.Generic.ISet<string>
|
||||||
|
override OpenTelemetry.Extensions.Propagators.B3Propagator.Inject<T>(OpenTelemetry.Context.Propagation.PropagationContext context, T carrier, System.Action<T, string, string> setter) -> void
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
// <copyright file="AssemblyInfo.cs" company="OpenTelemetry Authors">
|
||||||
|
// Copyright The OpenTelemetry Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License 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.
|
||||||
|
// </copyright>
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
#if SIGNED
|
||||||
|
[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Propagators.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898")]
|
||||||
|
#else
|
||||||
|
[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Propagators.Tests")]
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
// <copyright file="B3Propagator.cs" company="OpenTelemetry Authors">
|
||||||
|
// Copyright The OpenTelemetry Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License 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.
|
||||||
|
// </copyright>
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using OpenTelemetry.Context.Propagation;
|
||||||
|
using OpenTelemetry.Internal;
|
||||||
|
|
||||||
|
namespace OpenTelemetry.Extensions.Propagators
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A text map propagator for B3. See https://github.com/openzipkin/b3-propagation.
|
||||||
|
/// This has been lift-and-shifted as is from the <see cref="Context.Propagation.B3Propagator"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class B3Propagator : TextMapPropagator
|
||||||
|
{
|
||||||
|
internal const string XB3TraceId = "X-B3-TraceId";
|
||||||
|
internal const string XB3SpanId = "X-B3-SpanId";
|
||||||
|
internal const string XB3ParentSpanId = "X-B3-ParentSpanId";
|
||||||
|
internal const string XB3Sampled = "X-B3-Sampled";
|
||||||
|
internal const string XB3Flags = "X-B3-Flags";
|
||||||
|
internal const string XB3Combined = "b3";
|
||||||
|
internal const char XB3CombinedDelimiter = '-';
|
||||||
|
|
||||||
|
// Used as the upper ActivityTraceId.SIZE hex characters of the traceID. B3-propagation used to send
|
||||||
|
// ActivityTraceId.SIZE hex characters (8-bytes traceId) in the past.
|
||||||
|
internal const string UpperTraceId = "0000000000000000";
|
||||||
|
|
||||||
|
// Sampled values via the X_B3_SAMPLED header.
|
||||||
|
internal const string SampledValue = "1";
|
||||||
|
|
||||||
|
// Some old zipkin implementations may send true/false for the sampled header. Only use this for checking incoming values.
|
||||||
|
internal const string LegacySampledValue = "true";
|
||||||
|
|
||||||
|
// "Debug" sampled value.
|
||||||
|
internal const string FlagsValue = "1";
|
||||||
|
|
||||||
|
private static readonly HashSet<string> AllFields = new() { XB3TraceId, XB3SpanId, XB3ParentSpanId, XB3Sampled, XB3Flags };
|
||||||
|
|
||||||
|
private static readonly HashSet<string> SampledValues = new(StringComparer.Ordinal) { SampledValue, LegacySampledValue };
|
||||||
|
|
||||||
|
private readonly bool singleHeader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="B3Propagator"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public B3Propagator()
|
||||||
|
: this(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="B3Propagator"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="singleHeader">Determines whether to use single or multiple headers when extracting or injecting span context.</param>
|
||||||
|
public B3Propagator(bool singleHeader)
|
||||||
|
{
|
||||||
|
this.singleHeader = singleHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ISet<string> Fields => AllFields;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override PropagationContext Extract<T>(PropagationContext context, T carrier, Func<T, string, IEnumerable<string>> getter)
|
||||||
|
{
|
||||||
|
if (context.ActivityContext.IsValid())
|
||||||
|
{
|
||||||
|
// If a valid context has already been extracted, perform a noop.
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (carrier == null)
|
||||||
|
{
|
||||||
|
OpenTelemetryApiEventSource.Log.FailedToExtractActivityContext(nameof(B3Propagator), "null carrier");
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getter == null)
|
||||||
|
{
|
||||||
|
OpenTelemetryApiEventSource.Log.FailedToExtractActivityContext(nameof(B3Propagator), "null getter");
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.singleHeader)
|
||||||
|
{
|
||||||
|
return ExtractFromSingleHeader(context, carrier, getter);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ExtractFromMultipleHeaders(context, carrier, getter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Inject<T>(PropagationContext context, T carrier, Action<T, string, string> setter)
|
||||||
|
{
|
||||||
|
if (context.ActivityContext.TraceId == default || context.ActivityContext.SpanId == default)
|
||||||
|
{
|
||||||
|
OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "invalid context");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (carrier == null)
|
||||||
|
{
|
||||||
|
OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "null carrier");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setter == null)
|
||||||
|
{
|
||||||
|
OpenTelemetryApiEventSource.Log.FailedToInjectActivityContext(nameof(B3Propagator), "null setter");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.singleHeader)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append(context.ActivityContext.TraceId.ToHexString());
|
||||||
|
sb.Append(XB3CombinedDelimiter);
|
||||||
|
sb.Append(context.ActivityContext.SpanId.ToHexString());
|
||||||
|
if ((context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0)
|
||||||
|
{
|
||||||
|
sb.Append(XB3CombinedDelimiter);
|
||||||
|
sb.Append(SampledValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
setter(carrier, XB3Combined, sb.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setter(carrier, XB3TraceId, context.ActivityContext.TraceId.ToHexString());
|
||||||
|
setter(carrier, XB3SpanId, context.ActivityContext.SpanId.ToHexString());
|
||||||
|
if ((context.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0)
|
||||||
|
{
|
||||||
|
setter(carrier, XB3Sampled, SampledValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PropagationContext ExtractFromMultipleHeaders<T>(PropagationContext context, T carrier, Func<T, string, IEnumerable<string>> getter)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ActivityTraceId traceId;
|
||||||
|
var traceIdStr = getter(carrier, XB3TraceId)?.FirstOrDefault();
|
||||||
|
if (traceIdStr != null)
|
||||||
|
{
|
||||||
|
if (traceIdStr.Length == 16)
|
||||||
|
{
|
||||||
|
// This is an 8-byte traceID.
|
||||||
|
traceIdStr = UpperTraceId + traceIdStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActivitySpanId spanId;
|
||||||
|
var spanIdStr = getter(carrier, XB3SpanId)?.FirstOrDefault();
|
||||||
|
if (spanIdStr != null)
|
||||||
|
{
|
||||||
|
spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
var traceOptions = ActivityTraceFlags.None;
|
||||||
|
if (SampledValues.Contains(getter(carrier, XB3Sampled)?.FirstOrDefault())
|
||||||
|
|| FlagsValue.Equals(getter(carrier, XB3Flags)?.FirstOrDefault(), StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
traceOptions |= ActivityTraceFlags.Recorded;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PropagationContext(
|
||||||
|
new ActivityContext(traceId, spanId, traceOptions, isRemote: true),
|
||||||
|
context.Baggage);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PropagationContext ExtractFromSingleHeader<T>(PropagationContext context, T carrier, Func<T, string, IEnumerable<string>> getter)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var header = getter(carrier, XB3Combined)?.FirstOrDefault();
|
||||||
|
if (string.IsNullOrWhiteSpace(header))
|
||||||
|
{
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = header.Split(XB3CombinedDelimiter);
|
||||||
|
if (parts.Length < 2 || parts.Length > 4)
|
||||||
|
{
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
var traceIdStr = parts[0];
|
||||||
|
if (string.IsNullOrWhiteSpace(traceIdStr))
|
||||||
|
{
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (traceIdStr.Length == 16)
|
||||||
|
{
|
||||||
|
// This is an 8-byte traceID.
|
||||||
|
traceIdStr = UpperTraceId + traceIdStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
var traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan());
|
||||||
|
|
||||||
|
var spanIdStr = parts[1];
|
||||||
|
if (string.IsNullOrWhiteSpace(spanIdStr))
|
||||||
|
{
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
var spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan());
|
||||||
|
|
||||||
|
var traceOptions = ActivityTraceFlags.None;
|
||||||
|
if (parts.Length > 2)
|
||||||
|
{
|
||||||
|
var traceFlagsStr = parts[2];
|
||||||
|
if (SampledValues.Contains(traceFlagsStr)
|
||||||
|
|| FlagsValue.Equals(traceFlagsStr, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
traceOptions |= ActivityTraceFlags.Recorded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PropagationContext(
|
||||||
|
new ActivityContext(traceId, spanId, traceOptions, isRemote: true),
|
||||||
|
context.Baggage);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- OmniSharp/VS Code requires TargetFrameworks to be in descending order for IntelliSense and analysis. -->
|
||||||
|
<TargetFrameworks>net462;netstandard2.0</TargetFrameworks>
|
||||||
|
<Description>OpenTelemetry Extensions Propagators</Description>
|
||||||
|
<PackageTags>$(PackageTags);distributed-tracing;AspNet;AspNetCore;B3</PackageTags>
|
||||||
|
<IncludeInstrumentationHelpers>true</IncludeInstrumentationHelpers>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Api\Internal\OpenTelemetryApiEventSource.cs" Link="Includes\OpenTelemetryApiEventSource.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!--Do not run ApiCompat for net462/net6.0 as this is newly added. There is no existing contract for net462 against which we could compare the implementation.
|
||||||
|
Remove this property once we have released a stable net462/net6.0 version.-->
|
||||||
|
<PropertyGroup Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net6.0'">
|
||||||
|
<RunApiCompat>false</RunApiCompat>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Api\OpenTelemetry.Api.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<NoWarn>$(NoWarn),1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,399 @@
|
||||||
|
// <copyright file="B3PropagatorTest.cs" company="OpenTelemetry Authors">
|
||||||
|
// Copyright The OpenTelemetry Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License 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.
|
||||||
|
// </copyright>
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using OpenTelemetry.Context.Propagation;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace OpenTelemetry.Extensions.Propagators.Tests
|
||||||
|
{
|
||||||
|
public class B3PropagatorTest
|
||||||
|
{
|
||||||
|
private const string TraceIdBase16 = "ff000000000000000000000000000041";
|
||||||
|
private const string TraceIdBase16EightBytes = "0000000000000041";
|
||||||
|
private const string SpanIdBase16 = "ff00000000000041";
|
||||||
|
private const string InvalidId = "abcdefghijklmnop";
|
||||||
|
private const string InvalidSizeId = "0123456789abcdef00";
|
||||||
|
private const ActivityTraceFlags TraceOptions = ActivityTraceFlags.Recorded;
|
||||||
|
|
||||||
|
private static readonly ActivityTraceId TraceId = ActivityTraceId.CreateFromString(TraceIdBase16.AsSpan());
|
||||||
|
private static readonly ActivityTraceId TraceIdEightBytes = ActivityTraceId.CreateFromString(("0000000000000000" + TraceIdBase16EightBytes).AsSpan());
|
||||||
|
private static readonly ActivitySpanId SpanId = ActivitySpanId.CreateFromString(SpanIdBase16.AsSpan());
|
||||||
|
|
||||||
|
private static readonly Action<IDictionary<string, string>, string, string> Setter = (d, k, v) => d[k] = v;
|
||||||
|
private static readonly Func<IDictionary<string, string>, string, IEnumerable<string>> Getter =
|
||||||
|
(d, k) =>
|
||||||
|
{
|
||||||
|
d.TryGetValue(k, out var v);
|
||||||
|
return new string[] { v };
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly B3Propagator b3propagator = new();
|
||||||
|
private readonly B3Propagator b3PropagatorSingleHeader = new(true);
|
||||||
|
|
||||||
|
private readonly ITestOutputHelper output;
|
||||||
|
|
||||||
|
public B3PropagatorTest(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
this.output = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Serialize_SampledContext()
|
||||||
|
{
|
||||||
|
var carrier = new Dictionary<string, string>();
|
||||||
|
this.b3propagator.Inject(new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions), default), carrier, Setter);
|
||||||
|
this.ContainsExactly(carrier, new Dictionary<string, string> { { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, "1" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Serialize_NotSampledContext()
|
||||||
|
{
|
||||||
|
var carrier = new Dictionary<string, string>();
|
||||||
|
var context = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None);
|
||||||
|
this.output.WriteLine(context.ToString());
|
||||||
|
this.b3propagator.Inject(new PropagationContext(context, default), carrier, Setter);
|
||||||
|
this.ContainsExactly(carrier, new Dictionary<string, string> { { B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseMissingSampledAndMissingFlag()
|
||||||
|
{
|
||||||
|
var headersNotSampled = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 },
|
||||||
|
};
|
||||||
|
var spanContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(spanContext, default), this.b3propagator.Extract(default, headersNotSampled, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("1")]
|
||||||
|
[InlineData("true")]
|
||||||
|
public void ParseSampled(string sampledValue)
|
||||||
|
{
|
||||||
|
var headersSampled = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, sampledValue },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersSampled, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("0")]
|
||||||
|
[InlineData("false")]
|
||||||
|
[InlineData("something_else")]
|
||||||
|
public void ParseNotSampled(string sampledValue)
|
||||||
|
{
|
||||||
|
var headersNotSampled = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Sampled, sampledValue },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersNotSampled, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseFlag()
|
||||||
|
{
|
||||||
|
var headersFlagSampled = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Flags, "1" },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersFlagSampled, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseZeroFlag()
|
||||||
|
{
|
||||||
|
var headersFlagNotSampled = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, SpanIdBase16 }, { B3Propagator.XB3Flags, "0" },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersFlagNotSampled, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseEightBytesTraceId()
|
||||||
|
{
|
||||||
|
var headersEightBytes = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, TraceIdBase16EightBytes },
|
||||||
|
{ B3Propagator.XB3SpanId, SpanIdBase16 },
|
||||||
|
{ B3Propagator.XB3Sampled, "1" },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, TraceOptions, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersEightBytes, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseEightBytesTraceId_NotSampledSpanContext()
|
||||||
|
{
|
||||||
|
var headersEightBytes = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, TraceIdBase16EightBytes }, { B3Propagator.XB3SpanId, SpanIdBase16 },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, ActivityTraceFlags.None, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3propagator.Extract(default, headersEightBytes, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseInvalidTraceId()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, InvalidId }, { B3Propagator.XB3SpanId, SpanIdBase16 },
|
||||||
|
};
|
||||||
|
Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseInvalidTraceId_Size()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, InvalidSizeId }, { B3Propagator.XB3SpanId, SpanIdBase16 },
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseMissingTraceId()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string> { { B3Propagator.XB3SpanId, SpanIdBase16 }, };
|
||||||
|
Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseInvalidSpanId()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, InvalidId },
|
||||||
|
};
|
||||||
|
Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseInvalidSpanId_Size()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3TraceId, TraceIdBase16 }, { B3Propagator.XB3SpanId, InvalidSizeId },
|
||||||
|
};
|
||||||
|
Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseMissingSpanId()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string> { { B3Propagator.XB3TraceId, TraceIdBase16 } };
|
||||||
|
Assert.Equal(default, this.b3propagator.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Serialize_SampledContext_SingleHeader()
|
||||||
|
{
|
||||||
|
var carrier = new Dictionary<string, string>();
|
||||||
|
var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions);
|
||||||
|
this.b3PropagatorSingleHeader.Inject(new PropagationContext(activityContext, default), carrier, Setter);
|
||||||
|
this.ContainsExactly(carrier, new Dictionary<string, string> { { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Serialize_NotSampledContext_SingleHeader()
|
||||||
|
{
|
||||||
|
var carrier = new Dictionary<string, string>();
|
||||||
|
var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None);
|
||||||
|
this.output.WriteLine(activityContext.ToString());
|
||||||
|
this.b3PropagatorSingleHeader.Inject(new PropagationContext(activityContext, default), carrier, Setter);
|
||||||
|
this.ContainsExactly(carrier, new Dictionary<string, string> { { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseMissingSampledAndMissingFlag_SingleHeader()
|
||||||
|
{
|
||||||
|
var headersNotSampled = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}" },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersNotSampled, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseSampled_SingleHeader()
|
||||||
|
{
|
||||||
|
var headersSampled = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" },
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true), default),
|
||||||
|
this.b3PropagatorSingleHeader.Extract(default, headersSampled, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseZeroSampled_SingleHeader()
|
||||||
|
{
|
||||||
|
var headersNotSampled = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-0" },
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new PropagationContext(new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true), default),
|
||||||
|
this.b3PropagatorSingleHeader.Extract(default, headersNotSampled, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseFlag_SingleHeader()
|
||||||
|
{
|
||||||
|
var headersFlagSampled = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1" },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersFlagSampled, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseZeroFlag_SingleHeader()
|
||||||
|
{
|
||||||
|
var headersFlagNotSampled = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-0" },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceId, SpanId, ActivityTraceFlags.None, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersFlagNotSampled, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseEightBytesTraceId_SingleHeader()
|
||||||
|
{
|
||||||
|
var headersEightBytes = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{TraceIdBase16EightBytes}-{SpanIdBase16}-1" },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, TraceOptions, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersEightBytes, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseEightBytesTraceId_NotSampledSpanContext_SingleHeader()
|
||||||
|
{
|
||||||
|
var headersEightBytes = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{TraceIdBase16EightBytes}-{SpanIdBase16}" },
|
||||||
|
};
|
||||||
|
var activityContext = new ActivityContext(TraceIdEightBytes, SpanId, ActivityTraceFlags.None, isRemote: true);
|
||||||
|
Assert.Equal(new PropagationContext(activityContext, default), this.b3PropagatorSingleHeader.Extract(default, headersEightBytes, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseInvalidTraceId_SingleHeader()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{InvalidId}-{SpanIdBase16}" },
|
||||||
|
};
|
||||||
|
Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseInvalidTraceId_Size_SingleHeader()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{InvalidSizeId}-{SpanIdBase16}" },
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseMissingTraceId_SingleHeader()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string> { { B3Propagator.XB3Combined, $"-{SpanIdBase16}" } };
|
||||||
|
Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseInvalidSpanId_SingleHeader()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{TraceIdBase16}-{InvalidId}" },
|
||||||
|
};
|
||||||
|
Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseInvalidSpanId_Size_SingleHeader()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ B3Propagator.XB3Combined, $"{TraceIdBase16}-{InvalidSizeId}" },
|
||||||
|
};
|
||||||
|
Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseMissingSpanId_SingleHeader()
|
||||||
|
{
|
||||||
|
var invalidHeaders = new Dictionary<string, string> { { B3Propagator.XB3Combined, $"{TraceIdBase16}-" } };
|
||||||
|
Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fields_list()
|
||||||
|
{
|
||||||
|
ContainsExactly(
|
||||||
|
this.b3propagator.Fields,
|
||||||
|
new List<string> { B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ContainsExactly(ISet<string> list, List<string> items)
|
||||||
|
{
|
||||||
|
Assert.Equal(items.Count, list.Count);
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
Assert.Contains(item, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ContainsExactly(IDictionary<string, string> dict, IDictionary<string, string> items)
|
||||||
|
{
|
||||||
|
foreach (var d in dict)
|
||||||
|
{
|
||||||
|
this.output.WriteLine(d.Key + "=" + d.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(items.Count, dict.Count);
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
Assert.Contains(item, dict);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- OmniSharp/VS Code requires TargetFrameworks to be in descending order for IntelliSense and analysis. -->
|
||||||
|
<TargetFrameworks Condition="$(TARGET_FRAMEWORK) == ''">net6.0;netcoreapp3.1;net462</TargetFrameworks>
|
||||||
|
<TargetFrameworks Condition="$(TARGET_FRAMEWORK) != ''">$(TARGET_FRAMEWORK)</TargetFrameworks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPkgVer)" />
|
||||||
|
<PackageReference Include="xunit" Version="$(XUnitPkgVer)" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioPkgVer)">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<DotNetCliToolReference Include="dotnet-xunit" Version="$(DotNetXUnitCliVer)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Propagators\OpenTelemetry.Extensions.Propagators.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loading…
Reference in New Issue