Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Distributed tracing support #5078

Merged
merged 15 commits into from
Feb 26, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Text;
using Microsoft.Diagnostics.NETCore.Client;

namespace Microsoft.Diagnostics.Monitoring.EventPipe
{
public sealed class ActivitySourceConfiguration : MonitoringSourceConfiguration
{
private readonly double _samplingRatio;
private readonly string[] _activitySourceNames;

public ActivitySourceConfiguration(
double samplingRatio,
IEnumerable<string>? activitySourceNames)
{
_samplingRatio = samplingRatio;
_activitySourceNames = activitySourceNames?.ToArray() ?? Array.Empty<string>();
}

public override IList<EventPipeProvider> GetProviders()
{
StringBuilder filterAndPayloadSpecs = new();
foreach (string activitySource in _activitySourceNames)
{
if (string.IsNullOrEmpty(activitySource))
{
continue;
}

// Note: It isn't currently possible to get Events or Links off
// of Activity using this mechanism:
// Events=Events.*Enumerate;Links=Links.*Enumerate; See:
// https://github.com/dotnet/runtime/issues/102924

string sampler = string.Empty;

if (_samplingRatio < 1D)
{
sampler = $"-ParentRatioSampler({_samplingRatio})";
}

filterAndPayloadSpecs.AppendLine($"[AS]{activitySource}/Stop{sampler}:-TraceId;SpanId;ParentSpanId;ActivityTraceFlags;TraceStateString;Kind;DisplayName;StartTimeTicks=StartTimeUtc.Ticks;DurationTicks=Duration.Ticks;Status;StatusDescription;Tags=TagObjects.*Enumerate;ActivitySourceVersion=Source.Version");
}

// Note: Microsoft-Diagnostics-DiagnosticSource only supports a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We solve this problem btw with a shared string across our configurations. Since we are not planning on operating triggers and OTLP at the same time right now, it's not a concern but if we want to do all of these, we'll have to aggregate the otlp configuration with the other ones. Let's save that for the future.

// single listener. There can only be one
// ActivitySourceConfiguration, AspNetTriggerSourceConfiguration, or
// HttpRequestSourceConfiguration in play.
return new[] {
new EventPipeProvider(
DiagnosticSourceEventSource,
keywords: DiagnosticSourceEventSourceEvents | DiagnosticSourceEventSourceMessages,
eventLevel: EventLevel.Verbose,
arguments: new Dictionary<string, string>()
{
{ "FilterAndPayloadSpecs", filterAndPayloadSpecs.ToString() },
})
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Diagnostics;

namespace Microsoft.Diagnostics.Monitoring.EventPipe
{
internal readonly struct ActivityData
{
public ActivityData(
ActivitySourceData source,
string operationName,
string? displayName,
ActivityKind kind,
ActivityTraceId traceId,
ActivitySpanId spanId,
ActivitySpanId parentSpanId,
ActivityTraceFlags traceFlags,
string? traceState,
DateTime startTimeUtc,
DateTime endTimeUtc,
ActivityStatusCode status,
string? statusDescription)
{
if (string.IsNullOrEmpty(operationName))
{
throw new ArgumentNullException(nameof(operationName));
}

Source = source;
OperationName = operationName;
DisplayName = displayName;
Kind = kind;
TraceId = traceId;
SpanId = spanId;
ParentSpanId = parentSpanId;
TraceFlags = traceFlags;
TraceState = traceState;
StartTimeUtc = startTimeUtc;
EndTimeUtc = endTimeUtc;
Status = status;
StatusDescription = statusDescription;
}

public readonly ActivitySourceData Source;

public readonly string OperationName;

public readonly string? DisplayName;

public readonly ActivityKind Kind;

public readonly ActivityTraceId TraceId;

public readonly ActivitySpanId SpanId;

public readonly ActivitySpanId ParentSpanId;

public readonly ActivityTraceFlags TraceFlags;

public readonly string? TraceState;

public readonly DateTime StartTimeUtc;

public readonly DateTime EndTimeUtc;

public readonly ActivityStatusCode Status;

public readonly string? StatusDescription;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Collections.Generic;

namespace Microsoft.Diagnostics.Monitoring.EventPipe
{
internal ref struct ActivityPayload
{
public ActivityData ActivityData;

public ReadOnlySpan<KeyValuePair<string, object?>> Tags;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

namespace Microsoft.Diagnostics.Monitoring.EventPipe
{
internal sealed class ActivitySourceData
{
public ActivitySourceData(
string name,
string? version)
{
Name = name;
Version = version;
}

public string Name { get; }
public string? Version { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Diagnostics.Tracing;

namespace Microsoft.Diagnostics.Monitoring.EventPipe
{
internal class DistributedTracesPipeline : EventSourcePipeline<DistributedTracesPipelineSettings>
{
private readonly IActivityLogger[] _loggers;

public DistributedTracesPipeline(DiagnosticsClient client,
DistributedTracesPipelineSettings settings,
IEnumerable<IActivityLogger> loggers) : base(client, settings)
{
_loggers = loggers?.ToArray() ?? throw new ArgumentNullException(nameof(loggers));
}

protected override MonitoringSourceConfiguration CreateConfiguration()
=> new ActivitySourceConfiguration(Settings.SamplingRatio, Settings.Sources);

protected override async Task OnEventSourceAvailable(EventPipeEventSource eventSource, Func<Task> stopSessionAsync, CancellationToken token)
{
await ExecuteActivityLoggerActionAsync((logger) => logger.PipelineStarted(token)).ConfigureAwait(false);

eventSource.Dynamic.All += traceEvent => {
try
{
if (traceEvent.TryGetActivityPayload(out ActivityPayload activity))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we move the validation code here and get away with just one session? We are not validating prior to running anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried it. Doesn't work. When DS<9 if you try to give it the new sampling spec it just fails silently. There's no way to detect something didn't work.

{
foreach (IActivityLogger logger in _loggers)
{
try
{
logger.Log(
in activity.ActivityData,
activity.Tags);
}
catch (ObjectDisposedException)
{
}
}
}
}
catch (Exception)
{
}
};

using EventTaskSource<Action> sourceCompletedTaskSource = new(
taskComplete => taskComplete,
handler => eventSource.Completed += handler,
handler => eventSource.Completed -= handler,
token);

await sourceCompletedTaskSource.Task.ConfigureAwait(false);

await ExecuteActivityLoggerActionAsync((logger) => logger.PipelineStopped(token)).ConfigureAwait(false);
}

private async Task ExecuteActivityLoggerActionAsync(Func<IActivityLogger, Task> action)
{
foreach (IActivityLogger logger in _loggers)
{
try
{
await action(logger).ConfigureAwait(false);
}
catch (ObjectDisposedException)
{
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

namespace Microsoft.Diagnostics.Monitoring.EventPipe
{
internal class DistributedTracesPipelineSettings : EventSourcePipelineSettings
{
public double SamplingRatio { get; set; } = 1.0D;

public string[]? Sources { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Diagnostics.Monitoring.EventPipe
{
internal interface IActivityLogger
{
void Log(
in ActivityData activity,
ReadOnlySpan<KeyValuePair<string, object?>> tags);

Task PipelineStarted(CancellationToken token);
Task PipelineStopped(CancellationToken token);
}
}
Loading