Skip to content

Commit 85f672e

Browse files
committed
get context flowing across dotnet CLI calls
1 parent 95371bb commit 85f672e

File tree

3 files changed

+113
-22
lines changed

3 files changed

+113
-22
lines changed

src/Cli/dotnet/Activities.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@ namespace Microsoft.DotNet.Cli;
77
public static class Activities
88
{
99
public static ActivitySource s_source = new("dotnet-cli", Product.Version);
10+
11+
public const string DOTNET_CLI_TRACEPARENT = nameof(DOTNET_CLI_TRACEPARENT);
12+
public const string DOTNET_CLI_TRACESTATE = nameof(DOTNET_CLI_TRACESTATE);
1013
}
Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,54 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
#nullable disable
5-
4+
using System.Diagnostics;
65
using Microsoft.DotNet.Cli.Utils;
76
using Microsoft.DotNet.Cli.Utils.Extensions;
7+
using OpenTelemetry;
8+
using OpenTelemetry.Context.Propagation;
89

910
namespace Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
1011

1112
public class MuxerCommandResolver : ICommandResolver
1213
{
13-
public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
14+
public CommandSpec? Resolve(CommandResolverArguments commandResolverArguments)
1415
{
1516
if (commandResolverArguments.CommandName == Muxer.MuxerName)
1617
{
1718
var muxer = new Muxer();
1819
var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(
1920
commandResolverArguments.CommandArguments.OrEmptyIfNull());
20-
return new CommandSpec(muxer.MuxerPath, escapedArgs);
21+
var env = MakeActivityContextEnvironment();
22+
return new CommandSpec(muxer.MuxerPath, escapedArgs, env);
2123
}
2224
return null;
2325
}
26+
27+
private Dictionary<string, string>? MakeActivityContextEnvironment()
28+
{
29+
var currentActivity = Activity.Current;
30+
var currentBaggage = Baggage.Current;
31+
if (currentActivity == null)
32+
{
33+
return null;
34+
}
35+
var contextToInject = currentActivity.Context;
36+
var propagationContext = new PropagationContext(contextToInject, currentBaggage);
37+
var envDict = new Dictionary<string, string>();
38+
Propagators.DefaultTextMapPropagator.Inject(propagationContext, envDict, WriteTraceStateIntoEnv);
39+
return envDict;
40+
}
41+
42+
private void WriteTraceStateIntoEnv(Dictionary<string, string> dictionary, string key, string value)
43+
{
44+
switch (key)
45+
{
46+
case "traceparent":
47+
dictionary[Activities.DOTNET_CLI_TRACEPARENT] = value;
48+
break;
49+
case "tracestate":
50+
dictionary[Activities.DOTNET_CLI_TRACESTATE] = value;
51+
break;
52+
}
53+
}
2454
}

src/Cli/dotnet/Program.cs

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Microsoft.Extensions.EnvironmentAbstractions;
1818
using NuGet.Frameworks;
1919
using OpenTelemetry;
20+
using OpenTelemetry.Context.Propagation;
2021
using OpenTelemetry.Metrics;
2122
using OpenTelemetry.Resources;
2223
using OpenTelemetry.Trace;
@@ -54,12 +55,16 @@ public class Program
5455
.AddOtlpExporter()
5556
.Build();
5657

58+
private static ActivityContext s_parentActivityContext = default;
59+
private static ActivityKind s_activityKind = ActivityKind.Internal;
60+
5761

5862
public static int Main(string[] args)
5963
{
6064
// capture the time to we can compute muxer/host startup overhead
6165
DateTime mainTimeStamp = DateTime.Now;
62-
using var _mainActivity = Activities.s_source.StartActivity("main");
66+
(s_parentActivityContext, s_activityKind) = DeriveParentActivityContextFromEnv();
67+
using var _mainActivity = Activities.s_source.StartActivity("main", s_activityKind, s_parentActivityContext);
6368
using AutomaticEncodingRestorer _encodingRestorer = new();
6469

6570
// Setting output encoding is not available on those platforms
@@ -117,9 +122,44 @@ public static int Main(string[] args)
117122
}
118123
}
119124

125+
/// <summary>
126+
/// uses the OpenTelemetrySDK's Propagation API to derive the parent activity context and kind
127+
/// from the DOTNET_CLI_TRACEPARENT and DOTNET_CLI_TRACESTATE environment variables.
128+
/// </summary>
129+
/// <returns></returns>
130+
private static (ActivityContext parentActivityContext, ActivityKind kind) DeriveParentActivityContextFromEnv()
131+
{
132+
var traceParent = Env.GetEnvironmentVariable(Activities.DOTNET_CLI_TRACEPARENT);
133+
var traceState = Env.GetEnvironmentVariable(Activities.DOTNET_CLI_TRACESTATE);
134+
static IEnumerable<string> GetValueFromCarrier(Dictionary<string, IEnumerable<string>> carrier, string key)
135+
{
136+
return carrier.TryGetValue(key, out var value) ? value : Enumerable.Empty<string>();
137+
}
138+
139+
if (string.IsNullOrEmpty(traceParent))
140+
{
141+
return (default, ActivityKind.Internal);
142+
}
143+
var carriermap = new Dictionary<string, IEnumerable<string>>
144+
{
145+
{ "traceparent", [traceParent] },
146+
{ "tracestate", [traceState] }
147+
};
148+
149+
// Use the OpenTelemetry Propagator to extract the parent activity context and kind. For some reason this isn't set by the OTel SDK like docs say it should be.
150+
Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator([
151+
new TraceContextPropagator(),
152+
new BaggagePropagator()
153+
]));
154+
var parentActivityContext = Propagators.DefaultTextMapPropagator.Extract(default, carriermap, GetValueFromCarrier);
155+
var kind = parentActivityContext.ActivityContext.IsRemote ? ActivityKind.Server : ActivityKind.Internal;
156+
157+
return (parentActivityContext.ActivityContext, kind);
158+
}
159+
120160
private static void TrackHostStartup(DateTime mainTimeStamp)
121161
{
122-
var hostStartupActivity = Activities.s_source.CreateActivity("host-startup", ActivityKind.Server);
162+
var hostStartupActivity = Activities.s_source.CreateActivity("host-startup", s_activityKind, s_parentActivityContext);
123163
hostStartupActivity?.SetStartTime(Process.GetCurrentProcess().StartTime);
124164
hostStartupActivity?.SetEndTime(mainTimeStamp);
125165
hostStartupActivity?.SetStatus(ActivityStatusCode.Ok);
@@ -138,23 +178,51 @@ private static void SetupMSBuildEnvironmentInvariants()
138178
}
139179
}
140180

141-
internal static int ProcessArgs(string[] args)
181+
private static string GetCommandName(ParseResult r)
182+
{
183+
// walk the parent command tree to find the top-level command name and get the full command name for this parseresult
184+
List<string> parentNames = [r.CommandResult.Command.Name];
185+
var current = r.CommandResult.Parent;
186+
while (current is CommandResult parentCommandResult)
187+
{
188+
parentNames.Add(parentCommandResult.Command.Name);
189+
current = parentCommandResult.Parent;
190+
}
191+
parentNames.Reverse();
192+
return string.Join(' ', parentNames);
193+
}
194+
private static void SetDisplayName(Activity activity, ParseResult parseResult)
195+
{
196+
if (activity == null)
197+
{
198+
return;
199+
}
200+
var name = GetCommandName(parseResult);
201+
// Set the display name to the full command name
202+
activity.DisplayName = name;
203+
204+
// Set the command name as an attribute for better filtering in telemetry
205+
activity.SetTag("command.name", name);
206+
}
207+
208+
internal static int ProcessArgs(string[] args, ITelemetry telemetryClient = null)
142209
{
143210
ParseResult parseResult;
144-
using (var _parseActivity = Activities.s_source.StartActivity("parse"))
211+
using (var _parseActivity = Activities.s_source.StartActivity("parse", s_activityKind, s_parentActivityContext))
145212
{
146213
// If we get C# file path as the first argument, parse as `dotnet run file.cs`.
147214
parseResult = args is [{ } filePath, ..] && VirtualProjectBuildingCommand.IsValidEntryPointPath(filePath)
148215
? Parser.Instance.Parse(["run", .. args])
149216
: Parser.Instance.Parse(args);
150217

218+
SetDisplayName(_parseActivity, parseResult);
151219
// Avoid create temp directory with root permission and later prevent access in non sudo
152220
// This method need to be run very early before temp folder get created
153221
// https://github.com/dotnet/sdk/issues/20195
154222
SudoEnvironmentDirectoryOverride.OverrideEnvironmentVariableToTmp(parseResult);
155223
}
156224

157-
using (var _firstTimeUseActivity = Activities.s_source.StartActivity("first-time-use"))
225+
using (var _firstTimeUseActivity = Activities.s_source.StartActivity("first-time-use", s_activityKind, s_parentActivityContext))
158226
{
159227
IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel();
160228

@@ -221,17 +289,7 @@ internal static int ProcessArgs(string[] args)
221289
int exitCode;
222290
if (parseResult.CanBeInvoked())
223291
{
224-
using var _invocationActivity = Activities.s_source.StartActivity("invocation");
225-
// walk the parent command tree to find the top-level command name and get the full command name for this parseresult
226-
List<string> parentNames = [parseResult.CommandResult.Command.Name];
227-
var current = parseResult.CommandResult.Parent;
228-
while (current is CommandResult parentCommandResult)
229-
{
230-
parentNames.Add(parentCommandResult.Command.Name);
231-
current = parentCommandResult.Parent;
232-
}
233-
parentNames.Reverse();
234-
_invocationActivity?.DisplayName = string.Join(' ', parentNames);
292+
using var _invocationActivity = Activities.s_source.StartActivity("invocation", s_activityKind, s_parentActivityContext);
235293
try
236294
{
237295
exitCode = parseResult.Invoke();
@@ -246,14 +304,14 @@ internal static int ProcessArgs(string[] args)
246304
{
247305
try
248306
{
249-
var _lookupExternalCommandActivity = Activities.s_source.StartActivity("lookup-external-command");
307+
var _lookupExternalCommandActivity = Activities.s_source.StartActivity("lookup-external-command", s_activityKind, s_parentActivityContext);
250308
var resolvedCommand = CommandFactoryUsingResolver.Create(
251309
"dotnet-" + parseResult.GetValue(Parser.DotnetSubCommand),
252310
args.GetSubArguments(),
253311
FrameworkConstants.CommonFrameworks.NetStandardApp15);
254312
_lookupExternalCommandActivity?.Dispose();
255313

256-
var _executionActivity = Activities.s_source.StartActivity("execute-extensible-command");
314+
var _executionActivity = Activities.s_source.StartActivity("execute-extensible-command", s_activityKind, s_parentActivityContext);
257315
var result = resolvedCommand.Execute();
258316
_executionActivity?.Dispose();
259317

0 commit comments

Comments
 (0)