Skip to content
Merged
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);
}
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.

Copy link
Member Author

Choose a reason for hiding this comment

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

With regards to privacy - our overall plan for these spans is to not actually send them to the .NET SDK's telemetry collection. We plan to only ever collect the single 'main' activity at Program start, and that mostly because we need an Activity active in order for ActivityEvents to be sent, which is what our Telemetry data points become in an OTel world.

All other Activities we will conditionally disable with a Sampler when we do the full OTel integration - so they'll only be visible when local users are using the OTLP exporter, and at that point you as a user/operator are opting in to all of that data on your system so I believe the privacy constraints are satisfied.


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,10 +146,11 @@ 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();
Expand All @@ -159,6 +160,7 @@ internal async Task<NewCommandStatus> InvokeAsync(ParseResult parseResult, Cance

if (!args.IsForceFlagSpecified)
{
using var constraintResultsActivity = Activities.Source.StartActivity("validate-constraints");
var constraintResults = await constraintsEvaluation.ConfigureAwait(false);
if (constraintResults.Any())
{
Expand All @@ -173,7 +175,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
57 changes: 30 additions & 27 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema

Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
Expand All @@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple

There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
Expand Down Expand Up @@ -2628,6 +2628,9 @@ 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>
<data name="ToolInstallPackageIdMissing" xml:space="preserve">
<value>A package ID was not specified for tool installation.</value>
</data>
<data name="TestCommandUseSolution" xml:space="preserve">
<value>Specifying a solution for 'dotnet test' should be via '--solution'.</value>
</data>
Expand Down
Loading