Skip to content

Add activity spans for template creation and most tool usage #50163

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ dotnet_diagnostic.IDE0200.severity = none
dotnet_diagnostic.IDE0240.severity = warning

# Additional rules for template engine source code

# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.SpacingRules'
dotnet_analyzer_diagnostic.category-StyleCop.CSharp.SpacingRules.severity = none

[{src,test}/**{Microsoft.TemplateEngine.*,dotnet-new?*}/**.cs]
# Default analyzed API surface = 'public' (public APIs)
dotnet_code_quality.api_surface = public
Expand Down
16 changes: 14 additions & 2 deletions src/Cli/Microsoft.DotNet.Cli.Utils/Activities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,27 @@
namespace Microsoft.DotNet.Cli.Utils;

/// <summary>
/// Contains helpers for working with <see cref="System.Diagnostics.Activity">Activities</see> in the .NET CLI.
/// Contains helpers for working with <see cref="Activity">Activities</see> in the .NET CLI.
/// </summary>
public static class Activities
{

/// <summary>
/// The main entrypoint for creating <see cref="Activity">Activities</see> in the .NET CLI.
/// All activities created in the CLI should use this <see cref="ActivitySource"/>, to allow
/// consumers to easily filter and trace CLI activities.
/// </summary>
public static ActivitySource Source { get; } = new("dotnet-cli", Product.Version);

/// <summary>
/// The environment variable used to transfer the chain of parent activity IDs.
/// This should be used when constructing new sub-processes in order to
/// track spans across calls.
/// </summary>
public const string TRACEPARENT = nameof(TRACEPARENT);
/// <summary>
/// The environment variable used to transfer the trace state of the parent activities.
/// This should be used when constructing new sub-processes in order to
/// track spans across calls.
/// </summary>
public const string TRACESTATE = nameof(TRACESTATE);
Comment on lines +20 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

TRACEPARENT and TRACESTATE don't seem to be used in this PR yet.

I wouldn't use nameof here. If the constants are ever renamed (e.g. to mixed case) in the source code, their string values should remain unchanged. Instead #pragma warning disable CA1507.

In #49668 (comment), I asked about possible interference between instrumentation of the .NET SDK and instrumentation of tools. For example, when a system sets the TRACEPARENT environment variable, runs a tool via dotnet, and expects the tool to receive this TRACEPARENT value. If dotnet changes TRACEPARENT, and sends its own telemetry to Microsoft rather than to the same endpoint as the tool, then the trace will be somewhat broken as it will not be possible to find the correct parent span. I expect the trace ID will be preserved though.

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ internal InstantiateCommand(
Arity = new ArgumentArity(0, 999)
};

internal IReadOnlyList<Option> PassByOptions { get; } = new Option[]
{
internal IReadOnlyList<Option> PassByOptions { get; } =
[
SharedOptions.ForceOption,
SharedOptions.NameOption,
SharedOptions.DryRunOption,
SharedOptions.NoUpdateCheckOption
};
];

internal static Task<NewCommandStatus> ExecuteAsync(
NewCommandArgs newCommandArgs,
Expand All @@ -74,6 +74,7 @@ internal static async Task<IEnumerable<TemplateGroup>> GetTemplateGroupsAsync(
HostSpecificDataLoader hostSpecificDataLoader,
CancellationToken cancellationToken)
{
using var createTemplateGroupsActivity = Activities.Source.StartActivity("create-template-groups");
IReadOnlyList<ITemplateInfo> templates = await templatePackageManager.GetTemplatesAsync(cancellationToken).ConfigureAwait(false);
return TemplateGroup.FromTemplateList(CliTemplateInfo.FromTemplateInfo(templates, hostSpecificDataLoader));
}
Expand All @@ -84,6 +85,7 @@ internal static HashSet<TemplateCommand> GetTemplateCommand(
TemplatePackageManager templatePackageManager,
TemplateGroup templateGroup)
{
using var getTemplateActivity = Activities.Source.StartActivity("get-template-command");
//groups templates in the group by precedence
foreach (IGrouping<int, CliTemplateInfo> templateGrouping in templateGroup.Templates.GroupBy(g => g.Precedence).OrderByDescending(g => g.Key))
{
Expand Down Expand Up @@ -114,7 +116,7 @@ internal static HashSet<TemplateCommand> GetTemplateCommand(
templateGroup,
candidates);
}
return new HashSet<TemplateCommand>();
return [];
}

internal static void HandleNoMatchingTemplateGroup(InstantiateCommandArgs instantiateArgs, IEnumerable<TemplateGroup> templateGroups, IReporter reporter)
Expand Down Expand Up @@ -204,6 +206,8 @@ private static async Task<NewCommandStatus> ExecuteIntAsync(

return await templateListCoordinator.DisplayCommandDescriptionAsync(instantiateArgs, cancellationToken).ConfigureAwait(false);
}
using var createActivity = Activities.Source.StartActivity("instantiate-command");
createActivity?.DisplayName = $"Invoke '{instantiateArgs.ShortName}'";
Copy link
Contributor

Choose a reason for hiding this comment

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

Is instantiateArgs.ShortName already part of the telemetry?

Copy link
Contributor

Choose a reason for hiding this comment

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

OpenTelemetry.Exporter.OpenTelemetryProtocol exports Activity.DisplayName, not Activity.OperationName; so DisplayName is what matters for privacy.


IEnumerable<TemplateGroup> allTemplateGroups = await GetTemplateGroupsAsync(
templatePackageManager,
Expand Down Expand Up @@ -273,10 +277,11 @@ private static async Task<NewCommandStatus> HandleTemplateInstantiationAsync(
{
TemplateCommand templateCommandToRun = candidates.Single();
args.Command.Subcommands.Add(templateCommandToRun);

var templateParseActivity = Activities.Source.StartActivity("reparse-for-template");
ParseResult updatedParseResult = args.ParseResult.RootCommandResult.Command.Parse(
args.ParseResult.Tokens.Select(t => t.Value).ToArray(),
args.ParseResult.Configuration);
templateParseActivity?.Stop();
return await candidates.Single().InvokeAsync(updatedParseResult, cancellationToken).ConfigureAwait(false);
}
else if (candidates.Any())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Microsoft.TemplateEngine.Cli.Commands
internal class TemplateCommand : Command
{
private static readonly TimeSpan ConstraintEvaluationTimeout = TimeSpan.FromMilliseconds(1000);
private static readonly string[] _helpAliases = new[] { "-h", "/h", "--help", "-?", "/?" };
private static readonly string[] _helpAliases = ["-h", "/h", "--help", "-?", "/?"];
private readonly TemplatePackageManager _templatePackageManager;
private readonly IEngineEnvironmentSettings _environmentSettings;
private readonly BaseCommand _instantiateCommand;
Expand Down Expand Up @@ -146,19 +146,23 @@ internal static async Task<IReadOnlyList<TemplateConstraintResult>> ValidateCons

internal async Task<NewCommandStatus> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
using var templateInvocationActivity = Activities.Source.StartActivity("invoke-template");
TemplateCommandArgs args = new(this, _instantiateCommand, parseResult);
TemplateInvoker invoker = new(_environmentSettings, () => Console.ReadLine() ?? string.Empty);
TemplatePackageCoordinator packageCoordinator = new(_environmentSettings, _templatePackageManager);
TemplateConstraintManager constraintManager = new(_environmentSettings);
using TemplateConstraintManager constraintManager = new(_environmentSettings);
TemplatePackageDisplay templatePackageDisplay = new(Reporter.Output, Reporter.Error);

CancellationTokenSource cancellationTokenSource = new();
cancellationTokenSource.CancelAfter(ConstraintEvaluationTimeout);

#pragma warning disable CA2025 // Do not pass 'IDisposable' instances into unawaited tasks
Task<IReadOnlyList<TemplateConstraintResult>> constraintsEvaluation = ValidateConstraintsAsync(constraintManager, args.Template, args.IsForceFlagSpecified ? cancellationTokenSource.Token : cancellationToken);
#pragma warning restore CA2025 // Do not pass 'IDisposable' instances into unawaited tasks

if (!args.IsForceFlagSpecified)
{
using var constraintResultsActivity = Activities.Source.StartActivity("validate-constraints");
var constraintResults = await constraintsEvaluation.ConfigureAwait(false);
if (constraintResults.Any())
{
Expand All @@ -173,7 +177,7 @@ internal async Task<NewCommandStatus> InvokeAsync(ParseResult parseResult, Cance
Task<(string Id, string Version, string Provider)> builtInPackageCheck = packageCoordinator.ValidateBuiltInPackageAvailabilityAsync(args.Template, cancellationToken);
Task<CheckUpdateResult?> checkForUpdateTask = packageCoordinator.CheckUpdateForTemplate(args, cancellationToken);

Task[] tasksToWait = new Task[] { instantiateTask, builtInPackageCheck, checkForUpdateTask };
Task[] tasksToWait = [instantiateTask, builtInPackageCheck, checkForUpdateTask];

await Task.WhenAll(tasksToWait).ConfigureAwait(false);
Reporter.Output.WriteLine();
Expand Down
3 changes: 3 additions & 0 deletions src/Cli/Microsoft.TemplateEngine.Cli/TemplateInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal TemplateInvoker(

internal async Task<NewCommandStatus> InvokeTemplateAsync(TemplateCommandArgs templateArgs, CancellationToken cancellationToken)
{
using var invokerActivity = Activities.Source.StartActivity("invoker-invoking");
cancellationToken.ThrowIfCancellationRequested();

CliTemplateInfo templateToRun = templateArgs.Template;
Expand Down Expand Up @@ -158,6 +159,7 @@ private async Task<NewCommandStatus> CreateTemplateAsync(TemplateCommandArgs tem

try
{
using var templateCreationActivity = Activities.Source.StartActivity("actual-instantiate-template");
instantiateResult = await _templateCreator.InstantiateAsync(
templateArgs.Template,
templateArgs.Name,
Expand Down Expand Up @@ -306,6 +308,7 @@ private async Task<NewCommandStatus> CreateTemplateAsync(TemplateCommandArgs tem

private NewCommandStatus HandlePostActions(ITemplateCreationResult creationResult, TemplateCommandArgs args)
{
using var postActionActivity = Activities.Source.StartActivity("post-actions");
PostActionExecutionStatus result = _postActionDispatcher.Process(creationResult, args.IsDryRun, args.AllowScripts ?? AllowRunScripts.Prompt);

return result switch
Expand Down
31 changes: 11 additions & 20 deletions src/Cli/Microsoft.TemplateEngine.Cli/TemplateListCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ internal TemplateListCoordinator(
IEngineEnvironmentSettings engineEnvironmentSettings,
TemplatePackageManager templatePackageManager,
IHostSpecificDataLoader hostSpecificDataLoader)

{
_engineEnvironmentSettings = engineEnvironmentSettings ?? throw new ArgumentNullException(nameof(engineEnvironmentSettings));
_templatePackageManager = templatePackageManager ?? throw new ArgumentNullException(nameof(templatePackageManager));
_hostSpecificDataLoader = hostSpecificDataLoader ?? throw new ArgumentNullException(nameof(hostSpecificDataLoader));
_defaultLanguage = engineEnvironmentSettings.GetDefaultLanguage();
using var constraintManagerActivity = Activities.Source.StartActivity("create-constraints");
_constraintManager = new TemplateConstraintManager(_engineEnvironmentSettings);
}

Expand All @@ -48,7 +48,6 @@ internal async Task<NewCommandStatus> DisplayTemplateGroupListAsync(
ListTemplateResolver resolver = new(_constraintManager, _templatePackageManager, _hostSpecificDataLoader);
TemplateResolutionResult resolutionResult = await resolver.ResolveTemplatesAsync(args, _defaultLanguage, cancellationToken).ConfigureAwait(false);

//IReadOnlyDictionary<string, string?>? appliedParameterMatches = resolutionResult.GetAllMatchedParametersList();
if (resolutionResult.TemplateGroupsWithMatchingTemplateInfoAndParameters.Any())
{
Reporter.Output.WriteLine(LocalizableStrings.TemplatesFoundMatchingInputParameters, GetInputParametersString(args));
Expand All @@ -66,10 +65,10 @@ internal async Task<NewCommandStatus> DisplayTemplateGroupListAsync(
}
else
{
//if there is no criteria and filters it means that dotnet new list was run but there is no templates installed.
// If there is no criteria and filters, it means that dotnet new list was run but there are no templates installed.
if (args.ListNameCriteria == null && !args.AppliedFilters.Any())
{
//No templates installed.
// No templates installed.
Reporter.Output.WriteLine(LocalizableStrings.NoTemplatesFound);
Reporter.Output.WriteLine();
// To search for the templates on NuGet.org, run:
Expand All @@ -83,7 +82,7 @@ internal async Task<NewCommandStatus> DisplayTemplateGroupListAsync(
return NewCommandStatus.Success;
}

// at least one criteria was specified.
// At least one criteria was specified.
// No templates found matching the following input parameter(s): {0}.
Reporter.Error.WriteLine(
string.Format(
Expand Down Expand Up @@ -195,33 +194,30 @@ internal async Task<NewCommandStatus> DisplayCommandDescriptionAsync(
return NewCommandStatus.Success;
}

private static string GetInputParametersString(ListCommandArgs args/*, IReadOnlyDictionary<string, string?>? templateParameters = null*/)
private static string GetInputParametersString(ListCommandArgs args)
{
string separator = ", ";
IEnumerable<string> appliedFilters = args.AppliedFilters
.Select(filter => $"{args.GetFilterToken(filter)}='{args.GetFilterValue(filter)}'");

//IEnumerable<string> appliedTemplateParameters = templateParameters?
// .Select(param => string.IsNullOrWhiteSpace(param.Value) ? param.Key : $"{param.Key}='{param.Value}'") ?? Array.Empty<string>();

StringBuilder inputParameters = new();
string? mainCriteria = args.ListNameCriteria;
if (!string.IsNullOrWhiteSpace(mainCriteria))
{
inputParameters.Append($"'{mainCriteria}'");
if (appliedFilters.Any()/* || appliedTemplateParameters.Any()*/)
if (appliedFilters.Any())
{
inputParameters.Append(separator);
}
}
if (appliedFilters/*.Concat(appliedTemplateParameters)*/.Any())
if (appliedFilters.Any())
{
inputParameters.Append(string.Join(separator, appliedFilters/*.Concat(appliedTemplateParameters)*/));
inputParameters.Append(string.Join(separator, appliedFilters));
}
return inputParameters.ToString();
}

private static string GetPartialMatchReason(TemplateResolutionResult templateResolutionResult, ListCommandArgs args/*, IReadOnlyDictionary<string, string?>? templateParameters = null*/)
private static string GetPartialMatchReason(TemplateResolutionResult templateResolutionResult, ListCommandArgs args)
{
string separator = ", ";

Expand All @@ -230,15 +226,10 @@ private static string GetPartialMatchReason(TemplateResolutionResult templateRes
.Where(filter => filter.MismatchCriteria(templateResolutionResult))
.Select(filter => $"{args.GetFilterToken(filter)}='{args.GetFilterValue(filter)}'");

//IEnumerable<string> appliedTemplateParameters = templateParameters?
// .Where(parameter =>
// templateResolutionResult.IsParameterMismatchReason(parameter.Key))
// .Select(param => string.IsNullOrWhiteSpace(param.Value) ? param.Key : $"{param.Key}='{param.Value}'") ?? Array.Empty<string>();

StringBuilder inputParameters = new();
if (appliedFilters/*.Concat(appliedTemplateParameters)*/.Any())
if (appliedFilters.Any())
{
inputParameters.Append(string.Join(separator, appliedFilters/*.Concat(appliedTemplateParameters)*/));
inputParameters.Append(string.Join(separator, appliedFilters));
}
return inputParameters.ToString();
}
Expand Down
5 changes: 4 additions & 1 deletion src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2605,4 +2605,7 @@ Proceed?</value>
<value>Tool package download needs confirmation. Run in interactive mode or use the "--yes" command-line option to confirm.</value>
<comment>{Locked="--yes"}</comment>
</data>
</root>
<data name="ToolInstallPackageIdMissing" xml:space="preserve">
<value>A package ID was not specified for tool installation.</value>
</data>
</root>
15 changes: 14 additions & 1 deletion src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,23 @@ internal class ToolExecuteCommand(ParseResult result, ToolManifestFinder? toolMa

public override int Execute()
{
VersionRange versionRange = _parseResult.GetVersionRange();
VersionRange? versionRange = _parseResult.GetVersionRange();
PackageId packageId = new PackageId(_packageToolIdentityArgument.Id);

var toolLocationActivity = Activities.Source.StartActivity("find-tool");
toolLocationActivity?.SetTag("packageId", packageId.ToString());
toolLocationActivity?.SetTag("versionRange", versionRange?.ToString() ?? "latest");
Comment on lines +48 to +49
Copy link
Contributor

Choose a reason for hiding this comment

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

Are packageId and versionRange already part of the telemetry? packageId could easily refer to a private package. I assume these strings won't be hashed before they are exported over OTLP.

This PR is adding packageId to other activities too. I won't repeat this comment for those.


// Look in local tools manifest first, but only if version is not specified
if (versionRange == null)
{
var localToolsResolverCache = new LocalToolsResolverCache();

if (_toolManifestFinder.TryFindPackageId(packageId, out var toolManifestPackage))
{
toolLocationActivity?.SetTag("kind", "local");
toolLocationActivity?.Stop();

var toolPackageRestorer = new ToolPackageRestorer(
_toolPackageDownloader,
_sources,
Expand Down Expand Up @@ -82,6 +89,8 @@ public override int Execute()
additionalFeeds: _addSource);

(var bestVersion, var packageSource) = _toolPackageDownloader.GetNuGetVersion(packageLocation, packageId, _verbosity, versionRange, _restoreActionConfig);
toolLocationActivity?.SetTag("kind", "one-shot");
toolLocationActivity?.Stop();

// TargetFramework is null, which means to use the current framework. Global tools can override the target framework to use (or select assets for),
// but we don't support this for local or one-shot tools.
Expand Down Expand Up @@ -119,6 +128,10 @@ public override int Execute()
restoreActionConfig: _restoreActionConfig);
}

using var toolExecuteActivity = Activities.Source.StartActivity("execute-tool");
toolExecuteActivity?.SetTag("packageId", packageId.ToString());
toolExecuteActivity?.SetTag("version", toolPackage.Version.ToString());
toolExecuteActivity?.SetTag("source", toolPackage.Command.Runner);
Copy link
Contributor

Choose a reason for hiding this comment

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

"source" seems a strange tag name for toolPackage.Command.Runner.

var commandSpec = ToolCommandSpecCreator.CreateToolCommandSpec(toolPackage.Command.Name.Value, toolPackage.Command.Executable.Value, toolPackage.Command.Runner, _allowRollForward, _forwardArguments);
var command = CommandFactoryUsingResolver.Create(commandSpec);
var result = command.Execute();
Expand Down
Loading