Skip to content
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

FEATURE 2019014 - Gather telemetry on Agent Azure & Docker Container usage #4166

Merged
merged 7 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions src/Agent.Sdk/Agent.Sdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" Condition="$(CodeAnalysis)=='true'" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.7.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="4.4.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.4.0" />
<PackageReference Include="System.Security.Cryptography.Cng" Version="4.5.0" />
<PackageReference Include="vss-api-netcore" Version="$(VssApiVersion)" />
Expand Down
82 changes: 82 additions & 0 deletions src/Agent.Sdk/Util/AzureInstanceMetadataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Net.Http;
using System.Text;
using System.Web;

namespace Agent.Sdk.Util
{
class AzureInstanceMetadataProvider : IDisposable
{
private HttpClient _client;
private const string _version = "2021-02-01";
private const string _azureMetadataEndpoint = "http://169.254.169.254/metadata";

public AzureInstanceMetadataProvider()
{
_client = new HttpClient();
}

public void Dispose()
{
_client?.Dispose();
_client = null;
}

private HttpRequestMessage BuildRequest(string url, Dictionary<string, string> parameters)
{
UriBuilder builder = new UriBuilder(url);

NameValueCollection queryParameters = HttpUtility.ParseQueryString(builder.Query);

if (!parameters.ContainsKey("api-version"))
{
parameters.Add("api-version", _version);
}

foreach (KeyValuePair<string, string> entry in parameters)
{
queryParameters[entry.Key] = entry.Value;
}

builder.Query = queryParameters.ToString();

HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, builder.Uri);
request.Headers.Add("Metadata", "true");

return request;
}

public string GetMetadata(string category, Dictionary<string, string> parameters)
{
if (_client == null)
{
throw new ObjectDisposedException(nameof(AzureInstanceMetadataProvider));
}

HttpRequestMessage request = BuildRequest($"{_azureMetadataEndpoint}/{category}", parameters);
HttpResponseMessage response = _client.SendAsync(request).Result;

if (!response.IsSuccessStatusCode)
{
string errorText = response.Content.ReadAsStringAsync().Result;
throw new Exception($"Error retrieving metadata category { category }. Received status {response.StatusCode}: {errorText}");
}

return response.Content.ReadAsStringAsync().Result;
}

public bool HasMetadata()
{
try
{
return GetMetadata("instance", new Dictionary<string, string> { ["format"] = "text" }) != null;
}
catch (Exception)
Copy link
Contributor

Choose a reason for hiding this comment

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

Exception

No tracing, no filter on the exception, will make issues difficult to debug

{
return false;
}
}
}
}
56 changes: 56 additions & 0 deletions src/Agent.Sdk/Util/PlatformUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
Expand All @@ -15,6 +16,8 @@
using Microsoft.VisualStudio.Services.Agent.Util;
using Microsoft.Win32;
using Newtonsoft.Json;
using System.ServiceProcess;
using Agent.Sdk.Util;

namespace Agent.Sdk
{
Expand Down Expand Up @@ -355,6 +358,59 @@ public async static Task<bool> DoesSystemPersistsInNet6Whitelist()

return net6SupportedSystems.Any((s) => s.Equals(systemId));
}
public static bool DetectDockerContainer()
{
bool isDockerContainer = false;

try
{
if (PlatformUtil.RunningOnWindows)
{
// For Windows we check Container Execution Agent Service (cexecsvc) existence
var serviceName = "cexecsvc";
ServiceController[] scServices = ServiceController.GetServices();
if (scServices.Any(x => String.Equals(x.ServiceName, serviceName, StringComparison.OrdinalIgnoreCase) && x.Status == ServiceControllerStatus.Running))
{
isDockerContainer = true;
}
}
else
{
// In Unix in control group v1, we can identify if a process is running in a Docker
var initProcessCgroup = File.ReadLines("/proc/1/cgroup");
if (initProcessCgroup.Any(x => x.IndexOf(":/docker/", StringComparison.OrdinalIgnoreCase) >= 0))
{
isDockerContainer = true;
}
}
}
catch (Exception ex)
{
// Logging exception will be handled by JobRunner
throw;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Very low priority: try {} catch {} doesn't have any impact since the exception is just rethrown and "throw;" preserves the callstack. My suggestion is just remove the try / catch

return isDockerContainer;
}

public static bool DetectAzureVM()
{
bool isAzureVM = false;

try
{
// Metadata information endpoint can be used to check whether we're in Azure VM
// Additional info: https://learn.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux
using var metadataProvider = new AzureInstanceMetadataProvider();
if (metadataProvider.HasMetadata())
isAzureVM = true;
}
catch (Exception ex)
{
// Logging exception will be handled by JobRunner
throw;
}
return isAzureVM;
}
}

#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode()
Expand Down
12 changes: 12 additions & 0 deletions src/Agent.Worker/JobRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,18 @@ public async Task<TaskResult> RunAsync(Pipelines.AgentJobRequestMessage message,
jobContext.SetVariable(Constants.Variables.Agent.WorkFolder, HostContext.GetDirectory(WellKnownDirectory.Work), isFilePath: true);
jobContext.SetVariable(Constants.Variables.System.WorkFolder, HostContext.GetDirectory(WellKnownDirectory.Work), isFilePath: true);

try
{
jobContext.SetVariable(Constants.Variables.System.IsAzureVM, PlatformUtil.DetectAzureVM() ? "1" : "0");
jobContext.SetVariable(Constants.Variables.System.IsDockerContainer, PlatformUtil.DetectDockerContainer() ? "1" : "0");
}
catch (Exception ex)
{
// Error with telemetry shouldn't affect job run
Trace.Info($"Couldn't retrieve telemetry information");
Trace.Info(ex);
}

string toolsDirectory = HostContext.GetDirectory(WellKnownDirectory.Tools);
Directory.CreateDirectory(toolsDirectory);
jobContext.SetVariable(Constants.Variables.Agent.ToolsDirectory, toolsDirectory, isFilePath: true);
Expand Down
66 changes: 38 additions & 28 deletions src/Agent.Worker/TaskRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -575,36 +575,46 @@ private void PublishTelemetry(Definition taskDefinition, HandlerData handlerData
ArgUtil.NotNull(Task.Reference, nameof(Task.Reference));
ArgUtil.NotNull(taskDefinition.Data, nameof(taskDefinition.Data));

var useNode10 = AgentKnobs.UseNode10.GetValue(ExecutionContext).AsString();
var expectedExecutionHandler = (taskDefinition.Data.Execution?.All != null) ? string.Join(", ", taskDefinition.Data.Execution.All) : "";
var systemVersion = PlatformUtil.GetSystemVersion();
try
{
var useNode10 = AgentKnobs.UseNode10.GetValue(ExecutionContext).AsString();
var expectedExecutionHandler = (taskDefinition.Data.Execution?.All != null) ? string.Join(", ", taskDefinition.Data.Execution.All) : "";
var systemVersion = PlatformUtil.GetSystemVersion();

Dictionary<string, string> telemetryData = new Dictionary<string, string>
{
{ "TaskName", Task.Reference.Name },
{ "TaskId", Task.Reference.Id.ToString() },
{ "Version", Task.Reference.Version },
{ "OS", PlatformUtil.GetSystemId() ?? "" },
{ "OSVersion", systemVersion?.Name?.ToString() ?? "" },
{ "OSBuild", systemVersion?.Version?.ToString() ?? "" },
{ "ExpectedExecutionHandler", expectedExecutionHandler },
{ "RealExecutionHandler", handlerData.ToString() },
{ "UseNode10", useNode10 },
{ "JobId", ExecutionContext.Variables.System_JobId.ToString()},
{ "PlanId", ExecutionContext.Variables.Get(Constants.Variables.System.JobId)},
{ "AgentName", ExecutionContext.Variables.Get(Constants.Variables.Agent.Name)},
{ "MachineName", ExecutionContext.Variables.Get(Constants.Variables.Agent.MachineName)},
{ "IsSelfHosted", ExecutionContext.Variables.Get(Constants.Variables.Agent.IsSelfHosted)},
{ "IsAzureVM", ExecutionContext.Variables.Get(Constants.Variables.System.IsAzureVM)},
{ "IsDockerContainer", ExecutionContext.Variables.Get(Constants.Variables.System.IsDockerContainer)}
};

Dictionary<string, string> telemetryData = new Dictionary<string, string>
var cmd = new Command("telemetry", "publish");
cmd.Data = JsonConvert.SerializeObject(telemetryData, Formatting.None);
cmd.Properties.Add("area", "PipelinesTasks");
cmd.Properties.Add("feature", "ExecutionHandler");

var publishTelemetryCmd = new TelemetryCommandExtension();
publishTelemetryCmd.Initialize(HostContext);
publishTelemetryCmd.ProcessCommand(ExecutionContext, cmd);
}
catch (NullReferenceException ex)
{
{ "TaskName", Task.Reference.Name },
{ "TaskId", Task.Reference.Id.ToString() },
{ "Version", Task.Reference.Version },
{ "OS", PlatformUtil.GetSystemId() ?? "" },
{ "OSVersion", systemVersion?.Name?.ToString() ?? "" },
{ "OSBuild", systemVersion?.Version?.ToString() ?? "" },
{ "ExpectedExecutionHandler", expectedExecutionHandler },
{ "RealExecutionHandler", handlerData.ToString() },
{ "UseNode10", useNode10 },
{ "JobId", ExecutionContext.Variables.System_JobId.ToString()},
{ "PlanId", ExecutionContext.Variables.Get(Constants.Variables.System.JobId)},
{ "AgentName", ExecutionContext.Variables.Get(Constants.Variables.Agent.Name)},
{ "MachineName", ExecutionContext.Variables.Get(Constants.Variables.Agent.MachineName)},
{ "IsSelfHosted", ExecutionContext.Variables.Get(Constants.Variables.Agent.IsSelfHosted)}
};

var cmd = new Command("telemetry", "publish");
cmd.Data = JsonConvert.SerializeObject(telemetryData, Formatting.None);
cmd.Properties.Add("area", "PipelinesTasks");
cmd.Properties.Add("feature", "ExecutionHandler");

var publishTelemetryCmd = new TelemetryCommandExtension();
publishTelemetryCmd.Initialize(HostContext);
publishTelemetryCmd.ProcessCommand(ExecutionContext, cmd);
ExecutionContext.Debug($"ExecutionHandler telemetry wasn't published, because one of the variables is null");
ExecutionContext.Debug(ex.ToString());
}
}
}
}
4 changes: 4 additions & 0 deletions src/Microsoft.VisualStudio.Services.Agent/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,8 @@ public static class System
public static readonly string DefinitionName = "system.definitionName";
public static readonly string EnableAccessToken = "system.enableAccessToken";
public static readonly string HostType = "system.hosttype";
public static readonly string IsAzureVM = "system.isazurevm";
public static readonly string IsDockerContainer = "system.isdockercontainer";
public static readonly string JobAttempt = "system.jobAttempt";
public static readonly string JobDisplayName = "system.jobDisplayName";
public static readonly string JobId = "system.jobId";
Expand Down Expand Up @@ -567,6 +569,8 @@ public static class Task
System.DefinitionName,
System.EnableAccessToken,
System.HostType,
System.IsAzureVM,
System.IsDockerContainer,
System.JobAttempt,
System.JobDisplayName,
System.JobId,
Expand Down