Skip to content

Commit 441eb71

Browse files
authored
Merge pull request #34 from microsoft/alzollin/telemetry
Enhance telemetry and logging functionality.
2 parents 13f76f9 + 3258c4d commit 441eb71

File tree

10 files changed

+256
-88
lines changed

10 files changed

+256
-88
lines changed

src/winsdk-CLI/Winsdk.Cli/Program.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
using System.Text;
77
using Winsdk.Cli.Commands;
88
using Winsdk.Cli.Helpers;
9+
using Winsdk.Cli.Telemetry;
910
using Winsdk.Cli.Telemetry.Events;
11+
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
1012

1113
namespace Winsdk.Cli;
1214

@@ -62,9 +64,22 @@ static async Task<int> Main(string[] args)
6264

6365
var parseResult = rootCommand.Parse(args);
6466

65-
CommandExecutedEvent.Log(parseResult.CommandResult.Command.GetType().FullName!);
67+
try
68+
{
69+
CommandInvokedEvent.Log(parseResult.CommandResult);
70+
71+
var returnCode = await parseResult.InvokeAsync();
72+
73+
CommandCompletedEvent.Log(parseResult.CommandResult, returnCode);
6674

67-
return await parseResult.InvokeAsync();
75+
return returnCode;
76+
}
77+
catch (Exception ex)
78+
{
79+
TelemetryFactory.Get<ITelemetry>().LogException(parseResult.CommandResult.Command.Name, ex);
80+
Console.Error.WriteLine($"An unexpected error occurred: {ex.Message}");
81+
return 1;
82+
}
6883
}
6984

7085
internal static bool PromptYesNo(string message)

src/winsdk-CLI/Winsdk.Cli/Services/CertificateService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ public static async Task ValidatePublisherMatchAsync(string certificatePath, str
431431
$"Error: Publisher in {manifestPath} (CN={normalizedManifestPublisher}) does not match the publisher in the certificate {certificatePath} (CN={normalizedCertPublisher}).");
432432
}
433433
}
434-
catch (Exception ex) when (!(ex is InvalidOperationException))
434+
catch (Exception ex) when (ex is not InvalidOperationException)
435435
{
436436
throw new InvalidOperationException($"Failed to validate publisher match: {ex.Message}", ex);
437437
}
@@ -494,6 +494,6 @@ private async Task<string> InferPublisherAsync(
494494
return defaultPublisher;
495495
}
496496

497-
[GeneratedRegexAttribute(@"CN=([^,]+)", RegexOptions.IgnoreCase, "en-US")]
497+
[GeneratedRegex(@"CN=([^,]+)", RegexOptions.IgnoreCase, "en-US")]
498498
private static partial Regex CnFieldRegex();
499499
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Winsdk.Cli.Telemetry;
5+
6+
// Detection of CI: https://learn.microsoft.com/dotnet/core/tools/telemetry#continuous-integration-detection
7+
// From: https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Extensions.Telemetry/CIEnvironmentDetectorForTelemetry.cs
8+
internal sealed class CIEnvironmentDetectorForTelemetry
9+
{
10+
// Systems that provide boolean values only, so we can simply parse and check for true
11+
private static readonly string[] BooleanVariables =
12+
[
13+
// Azure Pipelines - https://docs.microsoft.com/azure/devops/pipelines/build/variables#system-variables-devops-services
14+
"TF_BUILD",
15+
16+
// GitHub Actions - https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
17+
"GITHUB_ACTIONS",
18+
19+
// AppVeyor - https://www.appveyor.com/docs/environment-variables/
20+
"APPVEYOR",
21+
22+
// A general-use flag - Many of the major players support this: AzDo, GitHub, GitLab, AppVeyor, Travis CI, CircleCI.
23+
// Given this, we could potentially remove all of these other options?
24+
"CI",
25+
26+
// Travis CI - https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
27+
"TRAVIS",
28+
29+
// CircleCI - https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables
30+
"CIRCLECI"
31+
];
32+
33+
// Systems where every variable must be present and not-null before returning true
34+
private static readonly string[][] AllNotNullVariables =
35+
[
36+
// AWS CodeBuild - https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
37+
["CODEBUILD_BUILD_ID", "AWS_REGION"],
38+
39+
// Jenkins - https://github.com/jenkinsci/jenkins/blob/master/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.groovy
40+
["BUILD_ID", "BUILD_URL"],
41+
42+
// Google Cloud Build - https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values#using_default_substitutions
43+
["BUILD_ID", "PROJECT_ID"],
44+
];
45+
46+
// Systems where the variable must be present and not-null
47+
private static readonly string[] IfNonNullVariables =
48+
[
49+
// TeamCity - https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters
50+
"TEAMCITY_VERSION",
51+
52+
// JetBrains Space - https://www.jetbrains.com/help/space/automation-environment-variables.html#general
53+
"JB_SPACE_API_URL"
54+
];
55+
56+
public static bool IsCIEnvironment()
57+
{
58+
foreach (string booleanVariable in BooleanVariables)
59+
{
60+
if (bool.TryParse(Environment.GetEnvironmentVariable(booleanVariable), out bool envVar) && envVar)
61+
{
62+
return true;
63+
}
64+
}
65+
66+
foreach (string[] variables in AllNotNullVariables)
67+
{
68+
if (variables.All(variable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))))
69+
{
70+
return true;
71+
}
72+
}
73+
74+
foreach (string variable in IfNonNullVariables)
75+
{
76+
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable)))
77+
{
78+
return true;
79+
}
80+
}
81+
82+
return false;
83+
}
84+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Diagnostics.Telemetry;
5+
using Microsoft.Diagnostics.Telemetry.Internal;
6+
using System.CommandLine.Parsing;
7+
using System.Diagnostics.Tracing;
8+
9+
namespace Winsdk.Cli.Telemetry.Events;
10+
11+
[EventData]
12+
internal class CommandCompletedEvent : EventBase
13+
{
14+
internal CommandCompletedEvent(CommandResult commandResult, DateTime finishedTime, int exitCode)
15+
{
16+
CommandName = commandResult.Command.GetType().FullName!;
17+
FinishedTime = finishedTime;
18+
ExitCode = exitCode;
19+
}
20+
21+
public string CommandName { get; private set; }
22+
23+
public DateTime FinishedTime { get; private set; }
24+
25+
public int ExitCode { get; }
26+
27+
public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage;
28+
29+
public override void ReplaceSensitiveStrings(Func<string, string> replaceSensitiveStrings)
30+
{
31+
CommandName = replaceSensitiveStrings(CommandName);
32+
}
33+
34+
public static void Log(CommandResult commandResult, int exitCode)
35+
{
36+
TelemetryFactory.Get<ITelemetry>().Log("CommandCompleted_Event", LogLevel.Critical, new CommandCompletedEvent(commandResult, DateTime.Now, exitCode));
37+
}
38+
}

src/winsdk-CLI/Winsdk.Cli/Telemetry/Events/CommandExecutedEvent.cs

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Diagnostics.Telemetry;
5+
using Microsoft.Diagnostics.Telemetry.Internal;
6+
using System.CommandLine;
7+
using System.CommandLine.Parsing;
8+
using System.Diagnostics.Tracing;
9+
using System.Text.Json;
10+
using System.Text.Json.Serialization;
11+
12+
namespace Winsdk.Cli.Telemetry.Events;
13+
14+
internal record CommandExecutionContext(Dictionary<string, string?> Arguments, Dictionary<string, string?> Options);
15+
16+
[JsonSerializable(typeof(CommandExecutionContext))]
17+
[JsonSourceGenerationOptions]
18+
internal partial class CommandInvokedEventJsonContext : JsonSerializerContext
19+
{
20+
}
21+
22+
[EventData]
23+
internal class CommandInvokedEvent : EventBase
24+
{
25+
internal CommandInvokedEvent(CommandResult commandResult, DateTime startedTime)
26+
{
27+
CommandName = commandResult.Command.GetType().FullName!;
28+
var argumentsDict = commandResult.Children
29+
.OfType<ArgumentResult>()
30+
.ToDictionary(a => a.Argument.Name, GetValue);
31+
var optionsDict = commandResult.Children
32+
.OfType<OptionResult>()
33+
.ToDictionary(o => o.Option.Name, GetValue);
34+
var commandExecutionContext = new CommandExecutionContext(argumentsDict, optionsDict);
35+
Context = JsonSerializer.Serialize(commandExecutionContext, CommandInvokedEventJsonContext.Default.CommandExecutionContext);
36+
StartedTime = startedTime;
37+
}
38+
39+
private string? GetValue(OptionResult o) => GetValue(o.Option.ValueType, o.Implicit, o.GetValueOrDefault<object?>());
40+
private string? GetValue(ArgumentResult a) => GetValue(a.Argument.ValueType, a.Implicit, a.GetValueOrDefault<object?>());
41+
42+
private static string? GetValue(Type valueType, bool isImplicit, object? value)
43+
{
44+
return isImplicit ? null : (valueType == typeof(string) ? "[string]" : value)?.ToString();
45+
}
46+
47+
public string CommandName { get; private set; }
48+
49+
public string Context { get; private set; }
50+
51+
public DateTime StartedTime { get; private set; }
52+
53+
public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage;
54+
55+
public override void ReplaceSensitiveStrings(Func<string, string> replaceSensitiveStrings)
56+
{
57+
CommandName = replaceSensitiveStrings(CommandName);
58+
Context = replaceSensitiveStrings(Context);
59+
}
60+
61+
public static void Log(CommandResult commandResult)
62+
{
63+
TelemetryFactory.Get<ITelemetry>().Log("CommandInvoked_Event", LogLevel.Critical, new CommandInvokedEvent(commandResult, DateTime.Now));
64+
}
65+
}

src/winsdk-CLI/Winsdk.Cli/Telemetry/Events/EmptyEvent.cs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,14 @@
44
using Microsoft.Diagnostics.Telemetry.Internal;
55
using System.Diagnostics.Tracing;
66

7-
namespace Winsdk.Cli.Telemetry;
7+
namespace Winsdk.Cli.Telemetry.Events;
88

99
[EventData]
10-
internal class EmptyEvent : EventBase
10+
internal class EmptyEvent(PartA_PrivTags tags) : EventBase
1111
{
12-
public override PartA_PrivTags PartA_PrivTags { get; }
12+
public override PartA_PrivTags PartA_PrivTags { get; } = tags;
1313

14-
public EmptyEvent(PartA_PrivTags tags)
15-
{
16-
PartA_PrivTags = tags;
17-
}
18-
19-
public override void ReplaceSensitiveStrings(Func<string?, string?> replaceSensitiveStrings)
14+
public override void ReplaceSensitiveStrings(Func<string, string> replaceSensitiveStrings)
2015
{
2116
// No sensitive string
2217
}

src/winsdk-CLI/Winsdk.Cli/Telemetry/Events/EventBase.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using Microsoft.Diagnostics.Telemetry.Internal;
55
using System.Diagnostics.Tracing;
66

7-
namespace Winsdk.Cli.Telemetry;
7+
namespace Winsdk.Cli.Telemetry.Events;
88

99
/// <summary>
1010
/// Base class for all telemetry events to ensure they are properly tagged.
@@ -26,6 +26,13 @@ public abstract PartA_PrivTags PartA_PrivTags
2626
get;
2727
}
2828

29+
/// <summary>
30+
/// Gets the app version from the assembly.
31+
/// </summary>
32+
public string AppVersion { get; } = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "Unknown";
33+
34+
public bool CI { get; } = CIEnvironmentDetectorForTelemetry.IsCIEnvironment();
35+
2936
/// <summary>
3037
/// Replaces all the strings in this event that may contain PII using the provided function.
3138
/// </summary>
@@ -37,5 +44,5 @@ public abstract PartA_PrivTags PartA_PrivTags
3744
/// <param name="replaceSensitiveStrings">
3845
/// A function that replaces all the sensitive strings in a given string with tokens
3946
/// </param>
40-
public abstract void ReplaceSensitiveStrings(Func<string?, string?> replaceSensitiveStrings);
47+
public abstract void ReplaceSensitiveStrings(Func<string, string> replaceSensitiveStrings);
4148
}

src/winsdk-CLI/Winsdk.Cli/Telemetry/ITelemetry.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System.Diagnostics.CodeAnalysis;
5+
using Winsdk.Cli.Telemetry.Events;
56

67
namespace Winsdk.Cli.Telemetry;
78

0 commit comments

Comments
 (0)