2019-10-10 00:52:42 -04:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.IO.Compression ;
using GitHub.DistributedTask.WebApi ;
using System.Linq ;
using System.Globalization ;
using System.Threading.Tasks ;
using Pipelines = GitHub . DistributedTask . Pipelines ;
using GitHub.Runner.Common ;
using GitHub.Runner.Sdk ;
namespace GitHub.Runner.Worker
{
[ServiceLocator(Default = typeof(DiagnosticLogManager))]
public interface IDiagnosticLogManager : IRunnerService
{
2019-12-09 16:11:00 -05:00
void UploadDiagnosticLogs ( IExecutionContext executionContext ,
2019-10-10 00:52:42 -04:00
IExecutionContext parentContext ,
Pipelines . AgentJobRequestMessage message ,
DateTime jobStartTimeUtc );
}
// This class manages gathering data for support logs, zipping the data, and uploading it.
// The files are created with the following folder structure:
// ..\_layout\_work\_temp
// \[job name]-support (supportRootFolder)
// \files (supportFolder)
// ...
// support.zip
public sealed class DiagnosticLogManager : RunnerService , IDiagnosticLogManager
{
private static string DateTimeFormat = "yyyyMMdd-HHmmss" ;
2019-12-09 16:11:00 -05:00
public void UploadDiagnosticLogs ( IExecutionContext executionContext ,
IExecutionContext parentContext ,
Pipelines . AgentJobRequestMessage message ,
DateTime jobStartTimeUtc )
2019-10-10 00:52:42 -04:00
{
executionContext . Debug ( "Starting diagnostic file upload." );
// Setup folders
// \_layout\_work\_temp\[jobname-support]
executionContext . Debug ( "Setting up diagnostic log folders." );
string tempDirectory = HostContext . GetDirectory ( WellKnownDirectory . Temp );
ArgUtil . Directory ( tempDirectory , nameof ( tempDirectory ));
string supportRootFolder = Path . Combine ( tempDirectory , message . JobName + "-support" );
Directory . CreateDirectory ( supportRootFolder );
// \_layout\_work\_temp\[jobname-support]\files
executionContext . Debug ( "Creating diagnostic log files folder." );
string supportFilesFolder = Path . Combine ( supportRootFolder , "files" );
Directory . CreateDirectory ( supportFilesFolder );
// Create the environment file
// \_layout\_work\_temp\[jobname-support]\files\environment.txt
var configurationStore = HostContext . GetService < IConfigurationStore >();
RunnerSettings settings = configurationStore . GetSettings ();
int runnerId = settings . AgentId ;
string runnerName = settings . AgentName ;
int poolId = settings . PoolId ;
// Copy worker diagnostic log files
List < string > workerDiagnosticLogFiles = GetWorkerDiagnosticLogFiles ( HostContext . GetDirectory ( WellKnownDirectory . Diag ), jobStartTimeUtc );
executionContext . Debug ( $"Copying {workerDiagnosticLogFiles.Count()} worker diagnostic logs." );
foreach ( string workerLogFile in workerDiagnosticLogFiles )
{
ArgUtil . File ( workerLogFile , nameof ( workerLogFile ));
string destination = Path . Combine ( supportFilesFolder , Path . GetFileName ( workerLogFile ));
File . Copy ( workerLogFile , destination );
}
// Copy runner diag log files
List < string > runnerDiagnosticLogFiles = GetRunnerDiagnosticLogFiles ( HostContext . GetDirectory ( WellKnownDirectory . Diag ), jobStartTimeUtc );
executionContext . Debug ( $"Copying {runnerDiagnosticLogFiles.Count()} runner diagnostic logs." );
foreach ( string runnerLogFile in runnerDiagnosticLogFiles )
{
ArgUtil . File ( runnerLogFile , nameof ( runnerLogFile ));
string destination = Path . Combine ( supportFilesFolder , Path . GetFileName ( runnerLogFile ));
File . Copy ( runnerLogFile , destination );
}
executionContext . Debug ( "Zipping diagnostic files." );
string buildNumber = executionContext . Variables . Build_Number ?? "UnknownBuildNumber" ;
string buildName = $"Build {buildNumber}" ;
string phaseName = executionContext . Variables . System_PhaseDisplayName ?? "UnknownPhaseName" ;
// zip the files
string diagnosticsZipFileName = $"{buildName}-{phaseName}.zip" ;
string diagnosticsZipFilePath = Path . Combine ( supportRootFolder , diagnosticsZipFileName );
ZipFile . CreateFromDirectory ( supportFilesFolder , diagnosticsZipFilePath );
// upload the json metadata file
executionContext . Debug ( "Uploading diagnostic metadata file." );
string metadataFileName = $"diagnostics-{buildName}-{phaseName}.json" ;
string metadataFilePath = Path . Combine ( supportFilesFolder , metadataFileName );
string phaseResult = GetTaskResultAsString ( executionContext . Result );
IOUtil . SaveObject ( new DiagnosticLogMetadata ( runnerName , runnerId , poolId , phaseName , diagnosticsZipFileName , phaseResult ), metadataFilePath );
// TODO: Remove the parentContext Parameter and replace this with executioncontext. Currently a bug exists where these files do not upload correctly using that context.
parentContext . QueueAttachFile ( type : CoreAttachmentType . DiagnosticLog , name : metadataFileName , filePath : metadataFilePath );
parentContext . QueueAttachFile ( type : CoreAttachmentType . DiagnosticLog , name : diagnosticsZipFileName , filePath : diagnosticsZipFilePath );
executionContext . Debug ( "Diagnostic file upload complete." );
}
private string GetTaskResultAsString ( TaskResult ? taskResult )
{
if (! taskResult . HasValue ) { return "Unknown" ; }
return taskResult . ToString ();
}
// The current solution is a hack. We need to rethink this and find a better one.
// The list of worker log files isn't available from the logger. It's also nested several levels deep.
// For this solution we deduce the applicable worker log files by comparing their create time to the start time of the job.
private List < string > GetWorkerDiagnosticLogFiles ( string diagnosticFolder , DateTime jobStartTimeUtc )
{
// Get all worker log files with a timestamp equal or greater than the start of the job
var workerLogFiles = new List < string >();
var directoryInfo = new DirectoryInfo ( diagnosticFolder );
// Sometimes the timing is off between the job start time and the time the worker log file is created.
// This adds a small buffer that provides some leeway in case the worker log file was created slightly
// before the time we log as job start time.
int bufferInSeconds = - 30 ;
DateTime searchTimeUtc = jobStartTimeUtc . AddSeconds ( bufferInSeconds );
foreach ( FileInfo file in directoryInfo . GetFiles (). Where ( f => f . Name . StartsWith ( Constants . Path . WorkerDiagnosticLogPrefix )))
{
// The format of the logs is:
// Worker_20171003-143110-utc.log
DateTime fileCreateTime = DateTime . ParseExact (
s : file . Name . Substring ( startIndex : Constants . Path . WorkerDiagnosticLogPrefix . Length , length : DateTimeFormat . Length ),
format : DateTimeFormat ,
provider : CultureInfo . InvariantCulture );
if ( fileCreateTime >= searchTimeUtc )
{
workerLogFiles . Add ( file . FullName );
}
}
return workerLogFiles ;
}
private List < string > GetRunnerDiagnosticLogFiles ( string diagnosticFolder , DateTime jobStartTimeUtc )
{
// Get the newest runner log file that created just before the start of the job
var runnerLogFiles = new List < string >();
var directoryInfo = new DirectoryInfo ( diagnosticFolder );
// The runner log that record the start point of the job should created before the job start time.
// The runner log may get paged if it reach size limit.
// We will only need upload 1 runner log file in 99%.
// There might be 1% we need to upload 2 runner log files.
String recentLog = null ;
DateTime recentTimeUtc = DateTime . MinValue ;
foreach ( FileInfo file in directoryInfo . GetFiles (). Where ( f => f . Name . StartsWith ( Constants . Path . RunnerDiagnosticLogPrefix )))
{
// The format of the logs is:
// Runner_20171003-143110-utc.log
if ( DateTime . TryParseExact (
s : file . Name . Substring ( startIndex : Constants . Path . RunnerDiagnosticLogPrefix . Length , length : DateTimeFormat . Length ),
format : DateTimeFormat ,
provider : CultureInfo . InvariantCulture ,
style : DateTimeStyles . None ,
result : out DateTime fileCreateTime ))
{
// always add log file created after the job start.
if ( fileCreateTime >= jobStartTimeUtc )
{
runnerLogFiles . Add ( file . FullName );
}
else if ( fileCreateTime > recentTimeUtc )
{
recentLog = file . FullName ;
recentTimeUtc = fileCreateTime ;
}
}
}
if (! String . IsNullOrEmpty ( recentLog ))
{
runnerLogFiles . Add ( recentLog );
}
return runnerLogFiles ;
}
}
}