Skip to content
This repository has been archived by the owner on Dec 25, 2024. It is now read-only.

Commit

Permalink
Collect usage telemetry via OpenTelemetry
Browse files Browse the repository at this point in the history
Telemetry collection can be opted out of by adding `-no-telemetry` to any command or by setting it globally using `falu config set no-telemetry true`
  • Loading branch information
mburumaxwell committed Jun 2, 2024
1 parent 42d518e commit be9ad39
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ updates:
groups:
microsoft:
patterns: ['Microsoft.*']
opentelemetry:
patterns: ['OpenTelemetry*']
system:
patterns: ['System.*']
xunit:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ jobs:
# https://github.blog/changelog/2023-10-30-accelerate-your-ci-cd-with-arm-based-hosted-runners-in-github-actions/
# TODO: consider using QEMU emulator for ARM?
if: ${{ !contains(matrix.arch, 'arm') }}
run: ./falu logout
run: ./falu logout --no-telemetry
working-directory: ${{ github.workspace }}/drop/${{ env.DOTNET_RID }}

- name: Upload Artifact (drop)
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"faluapp",
"idempotency",
"Mpesa",
"opentelemetry",
"Xunit"
]
}
3 changes: 3 additions & 0 deletions src/FaluCli/Commands/Config/ConfigCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ public async Task<int> InvokeAsync(InvocationContext context)
case "skip-update-check":
values.SkipUpdateChecks = bool.Parse(value);
break;
case "no-telemetry":
values.NoTelemetry = bool.Parse(value);
break;
case "retries":
values.Retries = int.Parse(value);
break;
Expand Down
3 changes: 3 additions & 0 deletions src/FaluCli/Config/ConfigValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ internal record ConfigValues
[JsonPropertyName("skip_update_checks")]
public bool SkipUpdateChecks { get; set; }

[JsonPropertyName("no_telemetry")]
public bool NoTelemetry { get; set; }

[JsonPropertyName("retries")]
public int Retries { get; set; } = 0;

Expand Down
73 changes: 72 additions & 1 deletion src/FaluCli/Extensions/CommandLineBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Falu.Commands.Login;
using Falu.Updates;
using Spectre.Console;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
Expand All @@ -14,11 +15,15 @@ namespace System.CommandLine.Builder;
/// </summary>
internal static class CommandLineBuilderExtensions
{
private static readonly Reflection.AssemblyName AssemblyName = typeof(CommandLineBuilder).Assembly.GetName();
private static readonly ActivitySource ActivitySource = new(AssemblyName.Name!, AssemblyName.Version!.ToString());

public static CommandLineBuilder UseFaluDefaults(this CommandLineBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));

return builder.UseVersionOption()
return builder.AddTracingMiddleware()
.UseVersionOption()
.UseHelp()
.UseEnvironmentVariableDirective()
.UseParseDirective()
Expand All @@ -30,6 +35,72 @@ public static CommandLineBuilder UseFaluDefaults(this CommandLineBuilder builder
.CancelOnProcessTermination();
}

private static CommandLineBuilder AddTracingMiddleware(this CommandLineBuilder builder)
{
// inspired by https://medium.com/@asimmon/instrumenting-system-commandline-based-net-applications-6d910f91b8a8

static string GetFullCommandName(ParseResult parseResult)
{
var names = new List<string>();
var result = parseResult.CommandResult;

while (result != null && result != parseResult.RootCommandResult)
{
names.Add(result.Command.Name);
result = result.Parent as CommandResult;
}

names.Reverse();

return string.Join(' ', names);
}

static string Redact(string value) => Constants.ApiKeyFormat.IsMatch(value) ? "***REDACTED***" : value;

return builder.AddMiddleware(async (context, next) =>
{
var activity = ActivitySource.StartActivity("Command", ActivityKind.Consumer);
if (activity is null)
{
await next(context);
return;
}

// Track command name, command arguments and username
var commandName = GetFullCommandName(context.ParseResult);
var commandArgs = string.Join(' ', context.ParseResult.Tokens.Select(t => Redact(t.Value)));
activity.DisplayName = commandName;
activity.SetTag("command.name", commandName);
activity.SetTag("command.args", commandArgs);
if (context.TryGetWorkspaceId(out var workspaceId)) activity.SetTag("workspace.id", workspaceId);
if (context.TryGetLiveMode(out var live)) activity.SetTag("live_mode", live.ToString());

try
{
await next(context);

activity?.SetStatus(ActivityStatusCode.Ok);
activity?.Stop();
}
catch (Exception ex)
{
var cancelled = context.GetCancellationToken().IsCancellationRequested;

if (!cancelled && activity.IsAllDataRequested)
{
activity.AddTag("exception.type", ex.GetType().FullName);
activity.AddTag("exception.message", ex.Message);
activity.AddTag("exception.stacktrace", ex.StackTrace);
}

activity.SetStatus(cancelled ? ActivityStatusCode.Ok : ActivityStatusCode.Error);
activity.Stop();

throw;
}
}, MiddlewareOrder.Default); // default = 0, anything less than that and the activity will be null because the host (which adds open telemetry) is registered at default too
}

private static void ExceptionHandler(Exception exception, InvocationContext context)
{
context.ExitCode = 1;
Expand Down
50 changes: 49 additions & 1 deletion src/FaluCli/Extensions/IHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,58 @@
using System.CommandLine.Hosting;
using Azure.Monitor.OpenTelemetry.Exporter;
using Falu.Config;
using Falu.Updates;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.CommandLine.Hosting;
using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Extensions.Hosting;

internal static class IHostBuilderExtensions
{
// this value is hardcoded because Microsoft does not consider the instrumentation key sensitive
private const string AppInsightsConnectionString = "InstrumentationKey=05728099-c2aa-411d-8f1c-e3aa9689daae;IngestionEndpoint=https://westeurope-5.in.applicationinsights.azure.com/;LiveEndpoint=https://westeurope.livediagnostics.monitor.azure.com/;ApplicationId=bb8bb015-675d-4658-a286-5d2108ca437a";

public static IHostBuilder AddOpenTelemetry(this IHostBuilder builder, ConfigValues configValues)
{
var invocation = builder.GetInvocationContext();
var disabled = invocation.IsNoTelemetry() || configValues.NoTelemetry;
if (disabled) return builder;

builder.ConfigureServices((context, services) =>
{
var environment = context.HostingEnvironment;
var configuration = context.Configuration;
var builder = services.AddOpenTelemetry();

// configure the resource
builder.ConfigureResource(builder =>
{
builder.AddAttributes([new("environment", environment.EnvironmentName)]);

// add detectors
builder.AddDetector(new OpenTelemetry.ResourceDetectors.Host.HostDetector());
builder.AddDetector(new OpenTelemetry.ResourceDetectors.ProcessRuntime.ProcessRuntimeDetector());

// add service name and version (should override any existing values)
builder.AddService("falu-cli", serviceVersion: VersioningHelper.ProductVersion);
});

// add tracing
builder.WithTracing(tracing =>
{
tracing.AddSource("System.CommandLine");
tracing.AddHttpClientInstrumentation();

// add exporter to Azure Monitor
var aics = configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"] ?? AppInsightsConnectionString;
tracing.AddAzureMonitorTraceExporter(options => options.ConnectionString = aics);
});
});

return builder;
}

// this exists to work around trimming which does not keep constructors by default in the System.CommandLine.Hosting library
// though Microsoft.Extensions.DependencyInjection does
public static IHostBuilder UseCommandHandlerTrimmable<
Expand Down
1 change: 1 addition & 0 deletions src/FaluCli/Extensions/InvocationContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace System.CommandLine;
internal static class InvocationContextExtensions
{
public static bool IsVerboseEnabled(this InvocationContext context) => context.ParseResult.ValueForOption<bool>("--verbose");
public static bool IsNoTelemetry(this InvocationContext context) => context.ParseResult.ValueForOption<bool>("--no-telemetry");
public static string? GetWorkspaceId(this InvocationContext context) => context.ParseResult.ValueForOption<string>("--workspace");
public static bool? GetLiveMode(this InvocationContext context) => context.ParseResult.ValueForOption<bool?>("--live");

Expand Down
5 changes: 5 additions & 0 deletions src/FaluCli/FaluCli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,17 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.2.0" />
<PackageReference Include="ByteSize" Version="2.1.2" />
<PackageReference Include="ClosedXML" Version="0.102.2" />
<PackageReference Include="CloudNative.CloudEvents.SystemTextJson" Version="2.7.1" />
<PackageReference Include="Falu" Version="1.13.1" />
<PackageReference Include="FileSignatures" Version="4.4.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.ResourceDetectors.Host" Version="0.1.0-alpha.3" />
<PackageReference Include="OpenTelemetry.ResourceDetectors.ProcessRuntime" Version="0.1.0-alpha.3" />
<PackageReference Include="SemanticVersioning" Version="2.0.2" />
<PackageReference Include="Spectre.Console.Json" Version="0.49.1" />
<PackageReference Include="System.CommandLine.Hosting" Version="0.4.0-alpha.22272.1" />
Expand Down
3 changes: 3 additions & 0 deletions src/FaluCli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
rootCommand.Description = "Official CLI tool for Falu.";
rootCommand.AddGlobalOption(["-v", "--verbose"], "Whether to output verbosely.", false);
rootCommand.AddGlobalOption<bool?>(["--skip-update-checks"], Res.OptionDescriptionSkipUpdateCheck); // nullable so as to allow checking if not specified
rootCommand.AddGlobalOption<bool>(["--no-telemetry"], Res.OptionDescriptionNoTelemetry);

var configValuesProvider = new ConfigValuesProvider();
var configValues = await configValuesProvider.GetConfigValuesAsync();
Expand Down Expand Up @@ -124,6 +125,8 @@
services.AddTransient<WebsocketHandler>();
});

host.AddOpenTelemetry(configValues);

// System.CommandLine library does not create a scope, so we should skip validation of scopes
host.UseDefaultServiceProvider(o => o.ValidateScopes = false);

Expand Down
9 changes: 9 additions & 0 deletions src/FaluCli/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/FaluCli/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ Example: my-awesome-op</value>
<data name="OptionDescriptionSkipUpdateCheck" xml:space="preserve">
<value>Whether to skip update checks. Using this option overrides any value set in the configuration. This value can also be set globally using 'falu config set skip-update-check true'</value>
</data>
<data name="OptionDescriptionNoTelemetry" xml:space="preserve">
<value>Whether to disable telemetry collection. Using this option overrides any value set in the configuration. This value can also be set globally using 'falu config set no-telemetry true'</value>
</data>
<data name="OptionDescriptionWorkspace" xml:space="preserve">
<value>The identifier of the workspace being accessed. Use this when you are logged into your account and you want to specify which workspace to target.
Example: wksp_610010be9228355f14ce6e08</value>
Expand Down

0 comments on commit be9ad39

Please sign in to comment.