From 0befa62f64fd14771446397b9c56e8d082a0dfca Mon Sep 17 00:00:00 2001 From: Yang Cao Date: Tue, 21 Feb 2023 09:55:47 -0500 Subject: [PATCH] Add job log upload support (#2447) * Refactor and add job log upload support * Rename method to be consistent --- src/Runner.Common/JobServer.cs | 9 + src/Runner.Common/JobServerQueue.cs | 70 ++++---- src/Sdk/WebApi/WebApi/Contracts.cs | 86 +++++---- src/Sdk/WebApi/WebApi/ResultsHttpClient.cs | 200 +++++++++++++-------- 4 files changed, 232 insertions(+), 133 deletions(-) diff --git a/src/Runner.Common/JobServer.cs b/src/Runner.Common/JobServer.cs index 085cc8497..9d8450378 100644 --- a/src/Runner.Common/JobServer.cs +++ b/src/Runner.Common/JobServer.cs @@ -32,6 +32,7 @@ namespace GitHub.Runner.Common Task CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, String type, String name, Stream uploadStream, CancellationToken cancellationToken); Task CreateStepSummaryAsync(string planId, string jobId, Guid stepId, string file, CancellationToken cancellationToken); Task CreateResultsStepLogAsync(string planId, string jobId, Guid stepId, string file, bool finalize, bool firstBlock, long lineCount, CancellationToken cancellationToken); + Task CreateResultsJobLogAsync(string planId, string jobId, string file, bool finalize, bool firstBlock, long lineCount, CancellationToken cancellationToken); Task CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken); Task CreateTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken); Task> UpdateTimelineRecordsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, IEnumerable records, CancellationToken cancellationToken); @@ -335,6 +336,14 @@ namespace GitHub.Runner.Common throw new InvalidOperationException("Results client is not initialized."); } + public Task CreateResultsJobLogAsync(string planId, string jobId, string file, bool finalize, bool firstBlock, long lineCount, CancellationToken cancellationToken) + { + if (_resultsClient != null) + { + return _resultsClient.UploadResultsJobLogAsync(planId, jobId, file, finalize, firstBlock, lineCount, cancellationToken: cancellationToken); + } + throw new InvalidOperationException("Results client is not initialized."); + } public Task CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken) { diff --git a/src/Runner.Common/JobServerQueue.cs b/src/Runner.Common/JobServerQueue.cs index f5f5a5494..1fadd99a8 100644 --- a/src/Runner.Common/JobServerQueue.cs +++ b/src/Runner.Common/JobServerQueue.cs @@ -85,6 +85,7 @@ namespace GitHub.Runner.Common private bool _firstConsoleOutputs = true; private bool _resultsClientInitiated = false; + private delegate Task ResultsFileUploadHandler(ResultsUploadFileInfo file); public override void Initialize(IHostContext hostContext) { @@ -252,12 +253,6 @@ namespace GitHub.Runner.Common return; } - if (timelineRecordId == _jobTimelineRecordId && String.Equals(type, CoreAttachmentType.ResultsLog, StringComparison.Ordinal)) - { - Trace.Verbose("Skipping job log {0} for record {1}", path, timelineRecordId); - return; - } - // all parameter not null, file path exist. var newFile = new ResultsUploadFileInfo() { @@ -503,8 +498,16 @@ namespace GitHub.Runner.Common } else if (String.Equals(file.Type, CoreAttachmentType.ResultsLog, StringComparison.OrdinalIgnoreCase)) { - Trace.Info($"Got a step log file to send to results service."); - await UploadResultsStepLogFile(file); + if (file.RecordId != _jobTimelineRecordId) + { + Trace.Info($"Got a step log file to send to results service."); + await UploadResultsStepLogFile(file); + } + else if (file.RecordId == _jobTimelineRecordId) + { + Trace.Info($"Got a job log file to send to results service."); + await UploadResultsJobLogFile(file); + } } } catch (Exception ex) @@ -813,40 +816,43 @@ namespace GitHub.Runner.Common private async Task UploadSummaryFile(ResultsUploadFileInfo file) { - bool uploadSucceed = false; - try + Trace.Info($"Starting to upload summary file to results service {file.Name}, {file.Path}"); + ResultsFileUploadHandler summaryHandler = async (file) => { - // Upload the step summary - Trace.Info($"Starting to upload summary file to results service {file.Name}, {file.Path}"); await _jobServer.CreateStepSummaryAsync(file.PlanId, file.JobId, file.RecordId, file.Path, CancellationToken.None); + }; - uploadSucceed = true; - } - finally - { - if (uploadSucceed && file.DeleteSource) - { - try - { - File.Delete(file.Path); - } - catch (Exception ex) - { - Trace.Info("Catch exception during delete success results uploaded summary file."); - Trace.Error(ex); - } - } - } + await UploadResultsFile(file, summaryHandler); } private async Task UploadResultsStepLogFile(ResultsUploadFileInfo file) + { + Trace.Info($"Starting upload of step log file to results service {file.Name}, {file.Path}"); + ResultsFileUploadHandler stepLogHandler = async (file) => + { + await _jobServer.CreateResultsStepLogAsync(file.PlanId, file.JobId, file.RecordId, file.Path, file.Finalize, file.FirstBlock, file.TotalLines, CancellationToken.None); + }; + + await UploadResultsFile(file, stepLogHandler); + } + + private async Task UploadResultsJobLogFile(ResultsUploadFileInfo file) + { + Trace.Info($"Starting upload of job log file to results service {file.Name}, {file.Path}"); + ResultsFileUploadHandler jobLogHandler = async (file) => + { + await _jobServer.CreateResultsJobLogAsync(file.PlanId, file.JobId, file.Path, file.Finalize, file.FirstBlock, file.TotalLines, CancellationToken.None); + }; + + await UploadResultsFile(file, jobLogHandler); + } + + private async Task UploadResultsFile(ResultsUploadFileInfo file, ResultsFileUploadHandler uploadHandler) { bool uploadSucceed = false; try { - Trace.Info($"Starting upload of step log file to results service {file.Name}, {file.Path}"); - await _jobServer.CreateResultsStepLogAsync(file.PlanId, file.JobId, file.RecordId, file.Path, file.Finalize, file.FirstBlock, file.TotalLines, CancellationToken.None); - + await uploadHandler(file); uploadSucceed = true; } finally diff --git a/src/Sdk/WebApi/WebApi/Contracts.cs b/src/Sdk/WebApi/WebApi/Contracts.cs index bc6361d62..9165d832f 100644 --- a/src/Sdk/WebApi/WebApi/Contracts.cs +++ b/src/Sdk/WebApi/WebApi/Contracts.cs @@ -28,6 +28,42 @@ namespace GitHub.Services.Results.Contracts public string BlobStorageType; } + [DataContract] + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public class StepSummaryMetadataCreate + { + [DataMember] + public string StepBackendId; + [DataMember] + public string WorkflowRunBackendId; + [DataMember] + public string WorkflowJobRunBackendId; + [DataMember] + public long Size; + [DataMember] + public string UploadedAt; + } + + [DataContract] + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public class GetSignedJobLogsURLRequest + { + [DataMember] + public string WorkflowJobRunBackendId; + [DataMember] + public string WorkflowRunBackendId; + } + + [DataContract] + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public class GetSignedJobLogsURLResponse + { + [DataMember] + public string LogsUrl; + [DataMember] + public string BlobStorageType; + } + [DataContract] [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] public class GetSignedStepLogsURLRequest @@ -47,41 +83,15 @@ namespace GitHub.Services.Results.Contracts [DataMember] public string LogsUrl; [DataMember] - public long SoftSizeLimit; - [DataMember] public string BlobStorageType; + [DataMember] + public long SoftSizeLimit; } [DataContract] [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] - public class StepSummaryMetadataCreate + public class JobLogsMetadataCreate { - [DataMember] - public string StepBackendId; - [DataMember] - public string WorkflowRunBackendId; - [DataMember] - public string WorkflowJobRunBackendId; - [DataMember] - public long Size; - [DataMember] - public string UploadedAt; - } - - [DataContract] - [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] - public class CreateStepSummaryMetadataResponse - { - [DataMember] - public bool Ok; - } - - [DataContract] - [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] - public class StepLogsMetadataCreate - { - [DataMember] - public string StepBackendId; [DataMember] public string WorkflowRunBackendId; [DataMember] @@ -94,7 +104,23 @@ namespace GitHub.Services.Results.Contracts [DataContract] [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] - public class CreateStepLogsMetadataResponse + public class StepLogsMetadataCreate + { + [DataMember] + public string WorkflowRunBackendId; + [DataMember] + public string WorkflowJobRunBackendId; + [DataMember] + public string StepBackendId; + [DataMember] + public string UploadedAt; + [DataMember] + public long LineCount; + } + + [DataContract] + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public class CreateMetadataResponse { [DataMember] public bool Ok; diff --git a/src/Sdk/WebApi/WebApi/ResultsHttpClient.cs b/src/Sdk/WebApi/WebApi/ResultsHttpClient.cs index 009308b98..b80ce4db3 100644 --- a/src/Sdk/WebApi/WebApi/ResultsHttpClient.cs +++ b/src/Sdk/WebApi/WebApi/ResultsHttpClient.cs @@ -24,7 +24,26 @@ namespace GitHub.Services.Results.Client m_formatter = new JsonMediaTypeFormatter(); } - public async Task GetStepSummaryUploadUrlAsync(string planId, string jobId, Guid stepId, CancellationToken cancellationToken) + // Get Sas URL calls + private async Task GetResultsSignedURLResponse(Uri uri, CancellationToken cancellationToken, R request) + { + using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri)) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token); + requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + using (HttpContent content = new ObjectContent(request, m_formatter)) + { + requestMessage.Content = content; + using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken)) + { + return await ReadJsonContentAsync(response, cancellationToken); + } + } + } + } + + private async Task GetStepSummaryUploadUrlAsync(string planId, string jobId, Guid stepId, CancellationToken cancellationToken) { var request = new GetSignedStepSummaryURLRequest() { @@ -33,25 +52,12 @@ namespace GitHub.Services.Results.Client StepBackendId = stepId.ToString() }; - var stepSummaryUploadRequest = new Uri(m_resultsServiceUrl, "twirp/results.services.receiver.Receiver/GetStepSummarySignedBlobURL"); + var getStepSummarySignedBlobURLEndpoint = new Uri(m_resultsServiceUrl, Constants.GetStepSummarySignedBlobURL); - using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, stepSummaryUploadRequest)) - { - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token); - requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - - using (HttpContent content = new ObjectContent(request, m_formatter)) - { - requestMessage.Content = content; - using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken)) - { - return await ReadJsonContentAsync(response, cancellationToken); - } - } - } + return await GetResultsSignedURLResponse(getStepSummarySignedBlobURLEndpoint, cancellationToken, request); } - public async Task GetStepLogUploadUrlAsync(string planId, string jobId, Guid stepId, CancellationToken cancellationToken) + private async Task GetStepLogUploadUrlAsync(string planId, string jobId, Guid stepId, CancellationToken cancellationToken) { var request = new GetSignedStepLogsURLRequest() { @@ -60,19 +66,43 @@ namespace GitHub.Services.Results.Client StepBackendId = stepId.ToString(), }; - var stepLogsUploadRequest = new Uri(m_resultsServiceUrl, "twirp/results.services.receiver.Receiver/GetStepLogsSignedBlobURL"); + var getStepLogsSignedBlobURLEndpoint = new Uri(m_resultsServiceUrl, Constants.GetStepLogsSignedBlobURL); - using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, stepLogsUploadRequest)) + return await GetResultsSignedURLResponse(getStepLogsSignedBlobURLEndpoint, cancellationToken, request); + } + + private async Task GetJobLogUploadUrlAsync(string planId, string jobId, CancellationToken cancellationToken) + { + var request = new GetSignedJobLogsURLRequest() + { + WorkflowJobRunBackendId = jobId, + WorkflowRunBackendId = planId, + }; + + var getJobLogsSignedBlobURLEndpoint = new Uri(m_resultsServiceUrl, Constants.GetJobLogsSignedBlobURL); + + return await GetResultsSignedURLResponse(getJobLogsSignedBlobURLEndpoint, cancellationToken, request); + } + + // Create metadata calls + + private async Task CreateMetadata(Uri uri, CancellationToken cancellationToken, R request, string timestamp) + { + using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri)) { requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token); requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - using (HttpContent content = new ObjectContent(request, m_formatter)) + using (HttpContent content = new ObjectContent(request, m_formatter)) { requestMessage.Content = content; using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken)) { - return await ReadJsonContentAsync(response, cancellationToken); + var jsonResponse = await ReadJsonContentAsync(response, cancellationToken); + if (!jsonResponse.Ok) + { + throw new Exception($"Failed to mark {typeof(R).Name} upload as complete, status code: {response.StatusCode}, ok: {jsonResponse.Ok}, timestamp: {timestamp}"); + } } } } @@ -80,7 +110,7 @@ namespace GitHub.Services.Results.Client private async Task StepSummaryUploadCompleteAsync(string planId, string jobId, Guid stepId, long size, CancellationToken cancellationToken) { - var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"); + var timestamp = DateTime.UtcNow.ToString(Constants.TimestampFormat); var request = new StepSummaryMetadataCreate() { WorkflowJobRunBackendId = jobId, @@ -90,31 +120,13 @@ namespace GitHub.Services.Results.Client UploadedAt = timestamp }; - var stepSummaryUploadCompleteRequest = new Uri(m_resultsServiceUrl, "twirp/results.services.receiver.Receiver/CreateStepSummaryMetadata"); - - using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, stepSummaryUploadCompleteRequest)) - { - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token); - requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - - using (HttpContent content = new ObjectContent(request, m_formatter)) - { - requestMessage.Content = content; - using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken)) - { - var jsonResponse = await ReadJsonContentAsync(response, cancellationToken); - if (!jsonResponse.Ok) - { - throw new Exception($"Failed to mark step summary upload as complete, status code: {response.StatusCode}, ok: {jsonResponse.Ok}, size: {size}, timestamp: {timestamp}"); - } - } - } - } + var createStepSummaryMetadataEndpoint = new Uri(m_resultsServiceUrl, Constants.CreateStepSummaryMetadata); + await CreateMetadata(createStepSummaryMetadataEndpoint, cancellationToken, request, timestamp); } private async Task StepLogUploadCompleteAsync(string planId, string jobId, Guid stepId, long lineCount, CancellationToken cancellationToken) { - var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"); + var timestamp = DateTime.UtcNow.ToString(Constants.TimestampFormat); var request = new StepLogsMetadataCreate() { WorkflowJobRunBackendId = jobId, @@ -124,29 +136,26 @@ namespace GitHub.Services.Results.Client LineCount = lineCount, }; - var stepLogsUploadCompleteRequest = new Uri(m_resultsServiceUrl, "twirp/results.services.receiver.Receiver/CreateStepLogsMetadata"); - - using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, stepLogsUploadCompleteRequest)) - { - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token); - requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - - using (HttpContent content = new ObjectContent(request, m_formatter)) - { - requestMessage.Content = content; - using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken)) - { - var jsonResponse = await ReadJsonContentAsync(response, cancellationToken); - if (!jsonResponse.Ok) - { - throw new Exception($"Failed to mark step log upload as complete, status code: {response.StatusCode}, ok: {jsonResponse.Ok}, timestamp: {timestamp}"); - } - } - } - } + var createStepLogsMetadataEndpoint = new Uri(m_resultsServiceUrl, Constants.CreateStepLogsMetadata); + await CreateMetadata(createStepLogsMetadataEndpoint, cancellationToken, request, timestamp); } - private async Task UploadFileAsync(string url, string blobStorageType, FileStream file, CancellationToken cancellationToken) + private async Task JobLogUploadCompleteAsync(string planId, string jobId, long lineCount, CancellationToken cancellationToken) + { + var timestamp = DateTime.UtcNow.ToString(Constants.TimestampFormat); + var request = new JobLogsMetadataCreate() + { + WorkflowJobRunBackendId = jobId, + WorkflowRunBackendId = planId, + UploadedAt = timestamp, + LineCount = lineCount, + }; + + var createJobLogsMetadataEndpoint = new Uri(m_resultsServiceUrl, Constants.CreateJobLogsMetadata); + await CreateMetadata(createJobLogsMetadataEndpoint, cancellationToken, request, timestamp); + } + + private async Task UploadBlockFileAsync(string url, string blobStorageType, FileStream file, CancellationToken cancellationToken) { // Upload the file to the url var request = new HttpRequestMessage(HttpMethod.Put, url) @@ -156,7 +165,7 @@ namespace GitHub.Services.Results.Client if (blobStorageType == BlobStorageTypes.AzureBlobStorage) { - request.Content.Headers.Add("x-ms-blob-type", "BlockBlob"); + request.Content.Headers.Add(Constants.AzureBlobTypeHeader, Constants.AzureBlockBlob); } using (var response = await SendAsync(request, HttpCompletionOption.ResponseHeadersRead, userState: null, cancellationToken)) @@ -177,7 +186,7 @@ namespace GitHub.Services.Results.Client }; if (blobStorageType == BlobStorageTypes.AzureBlobStorage) { - request.Content.Headers.Add("x-ms-blob-type", "AppendBlob"); + request.Content.Headers.Add(Constants.AzureBlobTypeHeader, Constants.AzureAppendBlob); request.Content.Headers.Add("Content-Length", "0"); } @@ -203,7 +212,7 @@ namespace GitHub.Services.Results.Client if (blobStorageType == BlobStorageTypes.AzureBlobStorage) { request.Content.Headers.Add("Content-Length", fileSize.ToString()); - request.Content.Headers.Add("x-ms-blob-sealed", finalize.ToString()); + request.Content.Headers.Add(Constants.AzureBlobSealedHeader, finalize.ToString()); } using (var response = await SendAsync(request, HttpCompletionOption.ResponseHeadersRead, userState: null, cancellationToken)) @@ -236,7 +245,7 @@ namespace GitHub.Services.Results.Client // Upload the file using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true)) { - var response = await UploadFileAsync(uploadUrlResponse.SummaryUrl, uploadUrlResponse.BlobStorageType, fileStream, cancellationToken); + var response = await UploadBlockFileAsync(uploadUrlResponse.SummaryUrl, uploadUrlResponse.BlobStorageType, fileStream, cancellationToken); } // Send step summary upload complete message @@ -253,9 +262,6 @@ namespace GitHub.Services.Results.Client throw new Exception("Failed to get step log upload url"); } - // Do we want to throw an exception here or should we just be uploading/truncating the data - var fileSize = new FileInfo(file).Length; - // Create the Append blob if (firstBlock) { @@ -263,6 +269,7 @@ namespace GitHub.Services.Results.Client } // Upload content + var fileSize = new FileInfo(file).Length; using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true)) { var response = await UploadAppendFileAsync(uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, fileStream, finalize, fileSize, cancellationToken); @@ -276,8 +283,59 @@ namespace GitHub.Services.Results.Client } } + // Handle file upload for job log + public async Task UploadResultsJobLogAsync(string planId, string jobId, string file, bool finalize, bool firstBlock, long lineCount, CancellationToken cancellationToken) + { + // Get the upload url + var uploadUrlResponse = await GetJobLogUploadUrlAsync(planId, jobId, cancellationToken); + if (uploadUrlResponse == null || uploadUrlResponse.LogsUrl == null) + { + throw new Exception("Failed to get job log upload url"); + } + + // Create the Append blob + if (firstBlock) + { + await CreateAppendFileAsync(uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, cancellationToken); + } + + // Upload content + var fileSize = new FileInfo(file).Length; + using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true)) + { + var response = await UploadAppendFileAsync(uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, fileStream, finalize, fileSize, cancellationToken); + } + + // Update metadata + if (finalize) + { + // Send step log upload complete message + await JobLogUploadCompleteAsync(planId, jobId, lineCount, cancellationToken); + } + } + private MediaTypeFormatter m_formatter; private Uri m_resultsServiceUrl; private string m_token; } + + // Constants specific to results + public static class Constants + { + public static readonly string TimestampFormat = "yyyy-MM-dd'T'HH:mm:ss.fffK"; + + public static readonly string ResultsReceiverTwirpEndpoint = "twirp/results.services.receiver.Receiver/"; + public static readonly string GetStepSummarySignedBlobURL = ResultsReceiverTwirpEndpoint + "GetStepSummarySignedBlobURL"; + public static readonly string CreateStepSummaryMetadata = ResultsReceiverTwirpEndpoint + "CreateStepSummaryMetadata"; + public static readonly string GetStepLogsSignedBlobURL = ResultsReceiverTwirpEndpoint + "GetStepLogsSignedBlobURL"; + public static readonly string CreateStepLogsMetadata = ResultsReceiverTwirpEndpoint + "CreateStepLogsMetadata"; + public static readonly string GetJobLogsSignedBlobURL = ResultsReceiverTwirpEndpoint + "GetJobLogsSignedBlobURL"; + public static readonly string CreateJobLogsMetadata = ResultsReceiverTwirpEndpoint + "CreateJobLogsMetadata"; + + public static readonly string AzureBlobSealedHeader = "x-ms-blob-sealed"; + public static readonly string AzureBlobTypeHeader = "x-ms-blob-type"; + public static readonly string AzureBlockBlob = "BlockBlob"; + public static readonly string AzureAppendBlob = "AppendBlob"; + } + }