From 19798ea07b6c584e7d6b40c8b29bd7913d1c8aac Mon Sep 17 00:00:00 2001 From: Arin Ghazarian Date: Thu, 5 Dec 2024 19:35:57 -0800 Subject: [PATCH 1/6] Add progress report for uploading to Azure blob storage --- src/Octoshift/Extensions/NumericExtensions.cs | 19 ++++++++++ src/Octoshift/Services/AzureApi.cs | 35 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/Octoshift/Extensions/NumericExtensions.cs diff --git a/src/Octoshift/Extensions/NumericExtensions.cs b/src/Octoshift/Extensions/NumericExtensions.cs new file mode 100644 index 000000000..cf34ee204 --- /dev/null +++ b/src/Octoshift/Extensions/NumericExtensions.cs @@ -0,0 +1,19 @@ +namespace OctoshiftCLI.Extensions; + +public static class NumericExtensions +{ + public static string ToLogFriendlySize(this long size) + { + const int kilobyte = 1024; + const int megabyte = 1024 * kilobyte; + const int gigabyte = 1024 * megabyte; + + return size switch + { + < kilobyte => $"{size:n0} bytes", + < megabyte => $"{size / (double)kilobyte:n0} KB", + < gigabyte => $"{size / (double)megabyte:n0} MB", + _ => $"{size / (double)gigabyte:n2} GB" + }; + } +} diff --git a/src/Octoshift/Services/AzureApi.cs b/src/Octoshift/Services/AzureApi.cs index a1b91b692..31873d58d 100644 --- a/src/Octoshift/Services/AzureApi.cs +++ b/src/Octoshift/Services/AzureApi.cs @@ -6,6 +6,7 @@ using Azure.Storage.Blobs.Models; using Azure.Storage.Blobs.Specialized; using Azure.Storage.Sas; +using OctoshiftCLI.Extensions; namespace OctoshiftCLI.Services; @@ -14,9 +15,12 @@ public class AzureApi private readonly HttpClient _client; private readonly BlobServiceClient _blobServiceClient; private readonly OctoLogger _log; + private readonly object _mutex = new(); private const string CONTAINER_PREFIX = "migration-archives"; private const int AUTHORIZATION_TIMEOUT_IN_HOURS = 48; private const int DEFAULT_BLOCK_SIZE = 4 * 1024 * 1024; + private const int UPLOAD_PROGRESS_REPORT_INTERVAL_IN_SECONDS = 10; + private DateTime _nextProgressReport; public AzureApi(HttpClient client, BlobServiceClient blobServiceClient, OctoLogger log) { @@ -48,9 +52,17 @@ public virtual async Task UploadToBlob(string fileName, byte[] content) public virtual async Task UploadToBlob(string fileName, Stream content) { + ArgumentNullException.ThrowIfNull(fileName, nameof(fileName)); + ArgumentNullException.ThrowIfNull(content); + var containerClient = await CreateBlobContainerAsync(); var blobClient = containerClient.GetBlobClient(fileName); + _nextProgressReport = DateTime.Now; + var progress = new Progress(); + var archiveSize = content.Length; + progress.ProgressChanged += (_, uploadedBytes) => LogProgress(uploadedBytes, archiveSize); + var options = new BlobUploadOptions { TransferOptions = new Azure.Storage.StorageTransferOptions() @@ -58,8 +70,9 @@ public virtual async Task UploadToBlob(string fileName, Stream content) InitialTransferSize = DEFAULT_BLOCK_SIZE, MaximumTransferSize = DEFAULT_BLOCK_SIZE }, + ProgressHandler = progress }; - + await blobClient.UploadAsync(content, options); return GetServiceSasUriForBlob(blobClient); } @@ -89,4 +102,24 @@ private Uri GetServiceSasUriForBlob(BlobClient blobClient) return blobClient.GenerateSasUri(sasBuilder); } + + private void LogProgress(long uploadedBytes, long totalBytes) + { + lock (_mutex) + { + if (DateTime.Now < _nextProgressReport) + { + return; + } + + _nextProgressReport = _nextProgressReport.AddSeconds(UPLOAD_PROGRESS_REPORT_INTERVAL_IN_SECONDS); + } + + var percentage = (int)(uploadedBytes * 100L / totalBytes); + var progressMessage = uploadedBytes > 0 + ? $", {uploadedBytes.ToLogFriendlySize()} out of {totalBytes.ToLogFriendlySize()} ({percentage}%) completed" + : ""; + + _log.LogInformation($"Archive upload in progress{progressMessage}..."); + } } From a03cd72c79456d47e0702f6b178f21794374cfa3 Mon Sep 17 00:00:00 2001 From: Arin Ghazarian Date: Fri, 6 Dec 2024 18:12:40 -0800 Subject: [PATCH 2/6] Initialize nextProgressReport on declaration --- src/Octoshift/Services/AzureApi.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Octoshift/Services/AzureApi.cs b/src/Octoshift/Services/AzureApi.cs index 31873d58d..ab4a2bd1f 100644 --- a/src/Octoshift/Services/AzureApi.cs +++ b/src/Octoshift/Services/AzureApi.cs @@ -20,7 +20,7 @@ public class AzureApi private const int AUTHORIZATION_TIMEOUT_IN_HOURS = 48; private const int DEFAULT_BLOCK_SIZE = 4 * 1024 * 1024; private const int UPLOAD_PROGRESS_REPORT_INTERVAL_IN_SECONDS = 10; - private DateTime _nextProgressReport; + private DateTime _nextProgressReport = DateTime.Now; public AzureApi(HttpClient client, BlobServiceClient blobServiceClient, OctoLogger log) { @@ -58,7 +58,6 @@ public virtual async Task UploadToBlob(string fileName, Stream content) var containerClient = await CreateBlobContainerAsync(); var blobClient = containerClient.GetBlobClient(fileName); - _nextProgressReport = DateTime.Now; var progress = new Progress(); var archiveSize = content.Length; progress.ProgressChanged += (_, uploadedBytes) => LogProgress(uploadedBytes, archiveSize); From 09cbbd6c9fead9923afd07d60dcdfbc33b409189 Mon Sep 17 00:00:00 2001 From: Arin Ghazarian Date: Fri, 6 Dec 2024 21:23:38 -0800 Subject: [PATCH 3/6] Add progress report for uploading to AWS S3 --- src/Octoshift/Services/AwsApi.cs | 60 ++++++++++++++++--- .../Octoshift/Services/AwsApiTests.cs | 35 +++++++---- .../bbs2gh/Factories/AwsApiFactoryTests.cs | 3 +- .../gei/Factories/AwsApiFactoryTests.cs | 3 +- src/bbs2gh/Factories/AwsApiFactory.cs | 6 +- src/gei/Factories/AwsApiFactory.cs | 6 +- 6 files changed, 87 insertions(+), 26 deletions(-) diff --git a/src/Octoshift/Services/AwsApi.cs b/src/Octoshift/Services/AwsApi.cs index 2df028dda..304ebbfdf 100644 --- a/src/Octoshift/Services/AwsApi.cs +++ b/src/Octoshift/Services/AwsApi.cs @@ -13,14 +13,22 @@ namespace OctoshiftCLI.Services; public class AwsApi : IDisposable { private const int AUTHORIZATION_TIMEOUT_IN_HOURS = 48; + private const int UPLOAD_PROGRESS_REPORT_INTERVAL_IN_SECONDS = 10; private readonly ITransferUtility _transferUtility; + private readonly object _mutex = new(); + private readonly OctoLogger _log; + private DateTime _nextProgressReport = DateTime.Now; - public AwsApi(ITransferUtility transferUtility) => _transferUtility = transferUtility; + public AwsApi(ITransferUtility transferUtility, OctoLogger log) + { + _transferUtility = transferUtility; + _log = log; + } #pragma warning disable CA2000 - public AwsApi(string awsAccessKeyId, string awsSecretAccessKey, string awsRegion = null, string awsSessionToken = null) - : this(new TransferUtility(BuildAmazonS3Client(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken))) + public AwsApi(OctoLogger log, string awsAccessKeyId, string awsSecretAccessKey, string awsRegion = null, string awsSessionToken = null) + : this(new TransferUtility(BuildAmazonS3Client(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken)), log) #pragma warning restore CA2000 { } @@ -51,20 +59,37 @@ public virtual async Task UploadToBucket(string bucketName, string fileN { try { - await _transferUtility.UploadAsync(fileName, bucketName, keyName); + var uploadRequest = new TransferUtilityUploadRequest + { + BucketName = bucketName, + Key = keyName, + FilePath = fileName + }; + return await UploadToBucket(uploadRequest); } catch (Exception ex) when (ex is TaskCanceledException or TimeoutException) { throw new OctoshiftCliException($"Upload of archive \"{fileName}\" to AWS timed out", ex); } - - return GetPreSignedUrlForFile(bucketName, keyName); } public virtual async Task UploadToBucket(string bucketName, Stream content, string keyName) { - await _transferUtility.UploadAsync(content, bucketName, keyName); - return GetPreSignedUrlForFile(bucketName, keyName); + var uploadRequest = new TransferUtilityUploadRequest + { + BucketName = bucketName, + Key = keyName, + InputStream = content + }; + return await UploadToBucket(uploadRequest); + } + + private async Task UploadToBucket(TransferUtilityUploadRequest uploadRequest) + { + uploadRequest.UploadProgressEvent += (_, args) => LogProgress(args.PercentDone, args.TransferredBytes, args.TotalBytes); + await _transferUtility.UploadAsync(uploadRequest); + + return GetPreSignedUrlForFile(uploadRequest.BucketName, uploadRequest.Key); } private string GetPreSignedUrlForFile(string bucketName, string keyName) @@ -81,6 +106,25 @@ private string GetPreSignedUrlForFile(string bucketName, string keyName) return _transferUtility.S3Client.GetPreSignedURL(urlRequest); } + private void LogProgress(int percentDone, long uploadedBytes, long totalBytes) + { + lock (_mutex) + { + if (DateTime.Now < _nextProgressReport) + { + return; + } + + _nextProgressReport = _nextProgressReport.AddSeconds(UPLOAD_PROGRESS_REPORT_INTERVAL_IN_SECONDS); + } + + var progressMessage = uploadedBytes > 0 + ? $", {uploadedBytes.ToLogFriendlySize()} out of {totalBytes.ToLogFriendlySize()} ({percentDone}%) completed" + : ""; + + _log.LogInformation($"Archive upload in progress{progressMessage}..."); + } + protected virtual void Dispose(bool disposing) { if (disposing) diff --git a/src/OctoshiftCLI.Tests/Octoshift/Services/AwsApiTests.cs b/src/OctoshiftCLI.Tests/Octoshift/Services/AwsApiTests.cs index 7a570da7f..4f849f6da 100644 --- a/src/OctoshiftCLI.Tests/Octoshift/Services/AwsApiTests.cs +++ b/src/OctoshiftCLI.Tests/Octoshift/Services/AwsApiTests.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; using Amazon.S3; @@ -8,6 +7,7 @@ using Amazon.S3.Transfer; using FluentAssertions; using Moq; +using OctoshiftCLI.Extensions; using OctoshiftCLI.Services; using Xunit; @@ -16,6 +16,8 @@ namespace OctoshiftCLI.Tests.Octoshift.Services; public class AwsApiTests { + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); + [Fact] public async Task UploadFileToBucket_Should_Succeed() { @@ -30,13 +32,15 @@ public async Task UploadFileToBucket_Should_Succeed() s3Client.Setup(m => m.GetPreSignedURL(It.IsAny())).Returns(url); transferUtility.Setup(m => m.S3Client).Returns(s3Client.Object); - using var awsApi = new AwsApi(transferUtility.Object); + using var awsApi = new AwsApi(transferUtility.Object, _mockOctoLogger.Object); var result = await awsApi.UploadToBucket(bucketName, fileName, keyName); // Assert result.Should().Be(url); - transferUtility.Verify(m => m.UploadAsync(fileName, bucketName, keyName, It.IsAny())); + transferUtility.Verify(m => m.UploadAsync( + It.Is(req => req.BucketName == bucketName && req.Key == keyName && req.FilePath == fileName), + It.IsAny())); } [Fact] @@ -44,9 +48,9 @@ public async Task UploadToBucket_Uploads_FileStream() { // Arrange var bucketName = "bucket"; - var bytes = Encoding.ASCII.GetBytes("here are some bytes"); + var expectedContent = "here are some bytes"; using var stream = new MemoryStream(); - stream.Write(bytes, 0, bytes.Length); + stream.Write(expectedContent.ToBytes()); var keyName = "key"; var url = "http://example.com/file.zip"; @@ -55,13 +59,16 @@ public async Task UploadToBucket_Uploads_FileStream() s3Client.Setup(m => m.GetPreSignedURL(It.IsAny())).Returns(url); transferUtility.Setup(m => m.S3Client).Returns(s3Client.Object); - using var awsApi = new AwsApi(transferUtility.Object); + using var awsApi = new AwsApi(transferUtility.Object, _mockOctoLogger.Object); var result = await awsApi.UploadToBucket(bucketName, stream, keyName); // Assert result.Should().Be(url); - transferUtility.Verify(m => m.UploadAsync(It.IsAny(), bucketName, keyName, It.IsAny())); + transferUtility.Verify(m => m.UploadAsync( + It.Is(req => + req.BucketName == bucketName && req.Key == keyName && (req.InputStream as MemoryStream).ToArray().GetString() == expectedContent), + It.IsAny())); } [Fact] @@ -69,7 +76,7 @@ public void It_Throws_If_Aws_Region_Is_Invalid() { // Arrange, Act const string awsRegion = "invalid-region"; - var awsApi = () => new AwsApi("awsAccessKeyId", "awsSecretAccessKey", awsRegion); + var awsApi = () => new AwsApi(_mockOctoLogger.Object, "awsAccessKeyId", "awsSecretAccessKey", awsRegion); // Assert awsApi.Should().Throw().WithMessage($"*{awsRegion}*"); @@ -88,9 +95,11 @@ public async Task UploadFileToBucket_Throws_If_TaskCanceledException_From_Timeou transferUtility.Setup(m => m.S3Client).Returns(s3Client.Object); - transferUtility.Setup(m => m.UploadAsync(fileName, bucketName, keyName, It.IsAny())).Throws(new TaskCanceledException()); + transferUtility.Setup(m => m.UploadAsync( + It.Is(req => req.BucketName == bucketName && req.Key == keyName && req.FilePath == fileName), + It.IsAny())).Throws(new TaskCanceledException()); - using var awsApi = new AwsApi(transferUtility.Object); + using var awsApi = new AwsApi(transferUtility.Object, _mockOctoLogger.Object); // Assert s3Client.Verify(m => m.GetPreSignedURL(It.IsAny()), Times.Never); @@ -112,9 +121,11 @@ public async Task UploadFileToBucket_Throws_If_TimeoutException_From_Timeout() transferUtility.Setup(m => m.S3Client).Returns(s3Client.Object); - transferUtility.Setup(m => m.UploadAsync(fileName, bucketName, keyName, It.IsAny())).Throws(new TimeoutException()); + transferUtility.Setup(m => m.UploadAsync( + It.Is(req => req.BucketName == bucketName && req.Key == keyName && req.FilePath == fileName), + It.IsAny())).Throws(new TimeoutException()); - using var awsApi = new AwsApi(transferUtility.Object); + using var awsApi = new AwsApi(transferUtility.Object, _mockOctoLogger.Object); // Assert s3Client.Verify(m => m.GetPreSignedURL(It.IsAny()), Times.Never); diff --git a/src/OctoshiftCLI.Tests/bbs2gh/Factories/AwsApiFactoryTests.cs b/src/OctoshiftCLI.Tests/bbs2gh/Factories/AwsApiFactoryTests.cs index 8a5c4a100..6fc119874 100644 --- a/src/OctoshiftCLI.Tests/bbs2gh/Factories/AwsApiFactoryTests.cs +++ b/src/OctoshiftCLI.Tests/bbs2gh/Factories/AwsApiFactoryTests.cs @@ -9,11 +9,12 @@ namespace OctoshiftCLI.Tests.bbs2gh.Factories; public class AwsApiFactoryTests { private readonly Mock _mockEnvironmentVariableProvider = TestHelpers.CreateMock(); + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); private readonly AwsApiFactory _awsApiFactory; public AwsApiFactoryTests() { - _awsApiFactory = new AwsApiFactory(_mockEnvironmentVariableProvider.Object); + _awsApiFactory = new AwsApiFactory(_mockEnvironmentVariableProvider.Object, _mockOctoLogger.Object); } [Fact] diff --git a/src/OctoshiftCLI.Tests/gei/Factories/AwsApiFactoryTests.cs b/src/OctoshiftCLI.Tests/gei/Factories/AwsApiFactoryTests.cs index 9256e147f..88c5a937a 100644 --- a/src/OctoshiftCLI.Tests/gei/Factories/AwsApiFactoryTests.cs +++ b/src/OctoshiftCLI.Tests/gei/Factories/AwsApiFactoryTests.cs @@ -9,11 +9,12 @@ namespace OctoshiftCLI.Tests.GithubEnterpriseImporter.Factories; public class AwsApiFactoryTests { private readonly Mock _mockEnvironmentVariableProvider = TestHelpers.CreateMock(); + private readonly Mock _mockOctoLogger = TestHelpers.CreateMock(); private readonly AwsApiFactory _awsApiFactory; public AwsApiFactoryTests() { - _awsApiFactory = new AwsApiFactory(_mockEnvironmentVariableProvider.Object); + _awsApiFactory = new AwsApiFactory(_mockEnvironmentVariableProvider.Object, _mockOctoLogger.Object); } [Fact] diff --git a/src/bbs2gh/Factories/AwsApiFactory.cs b/src/bbs2gh/Factories/AwsApiFactory.cs index c657d83de..77de6f1cb 100644 --- a/src/bbs2gh/Factories/AwsApiFactory.cs +++ b/src/bbs2gh/Factories/AwsApiFactory.cs @@ -5,10 +5,12 @@ namespace OctoshiftCLI.BbsToGithub.Factories; public class AwsApiFactory { private readonly EnvironmentVariableProvider _environmentVariableProvider; + private readonly OctoLogger _octoLogger; - public AwsApiFactory(EnvironmentVariableProvider environmentVariableProvider) + public AwsApiFactory(EnvironmentVariableProvider environmentVariableProvider, OctoLogger octoLogger) { _environmentVariableProvider = environmentVariableProvider; + _octoLogger = octoLogger; } public virtual AwsApi Create(string awsRegion = null, string awsAccessKeyId = null, string awsSecretAccessKey = null, string awsSessionToken = null) @@ -18,6 +20,6 @@ public virtual AwsApi Create(string awsRegion = null, string awsAccessKeyId = nu awsSessionToken ??= _environmentVariableProvider.AwsSessionToken(false); awsRegion ??= _environmentVariableProvider.AwsRegion(); - return new AwsApi(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken); + return new AwsApi(_octoLogger, awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken); } } diff --git a/src/gei/Factories/AwsApiFactory.cs b/src/gei/Factories/AwsApiFactory.cs index cb71759d2..9a2f28580 100644 --- a/src/gei/Factories/AwsApiFactory.cs +++ b/src/gei/Factories/AwsApiFactory.cs @@ -5,10 +5,12 @@ namespace OctoshiftCLI.GithubEnterpriseImporter.Factories; public class AwsApiFactory { private readonly EnvironmentVariableProvider _environmentVariableProvider; + private readonly OctoLogger _octoLogger; - public AwsApiFactory(EnvironmentVariableProvider environmentVariableProvider) + public AwsApiFactory(EnvironmentVariableProvider environmentVariableProvider, OctoLogger octoLogger) { _environmentVariableProvider = environmentVariableProvider; + _octoLogger = octoLogger; } public virtual AwsApi Create(string awsRegion = null, string awsAccessKeyId = null, string awsSecretAccessKey = null, string awsSessionToken = null) @@ -18,6 +20,6 @@ public virtual AwsApi Create(string awsRegion = null, string awsAccessKeyId = nu awsSessionToken ??= _environmentVariableProvider.AwsSessionToken(false); awsRegion ??= _environmentVariableProvider.AwsRegion(); - return new AwsApi(awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken); + return new AwsApi(_octoLogger, awsAccessKeyId, awsSecretAccessKey, awsRegion, awsSessionToken); } } From 83c8aafb7067a25bbf0cdd09ad54b2805afdd6cd Mon Sep 17 00:00:00 2001 From: Arin Ghazarian Date: Fri, 6 Dec 2024 21:23:54 -0800 Subject: [PATCH 4/6] Linting --- src/Octoshift/Services/AzureApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Octoshift/Services/AzureApi.cs b/src/Octoshift/Services/AzureApi.cs index ab4a2bd1f..55f63cfd6 100644 --- a/src/Octoshift/Services/AzureApi.cs +++ b/src/Octoshift/Services/AzureApi.cs @@ -71,7 +71,7 @@ public virtual async Task UploadToBlob(string fileName, Stream content) }, ProgressHandler = progress }; - + await blobClient.UploadAsync(content, options); return GetServiceSasUriForBlob(blobClient); } From 56c25bae6e90710478ccdd904ab0315b05959f46 Mon Sep 17 00:00:00 2001 From: Arin Ghazarian Date: Fri, 6 Dec 2024 21:28:25 -0800 Subject: [PATCH 5/6] RELEASENOTES --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8b1378917..e7dc4f70d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1 +1 @@ - +- Add progress report to `gh [gei|bbs2gh] migrate-repo` command when uploading migration archives to Azure Blob or AWS S3 From 79ee21c355eed3e9843ef2471c1043dfc0c4e92c Mon Sep 17 00:00:00 2001 From: Arin Ghazarian Date: Tue, 10 Dec 2024 17:25:14 -0800 Subject: [PATCH 6/6] Make OctoLogger thread safe --- src/Octoshift/Services/OctoLogger.cs | 74 ++++++++++++++++++---------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/src/Octoshift/Services/OctoLogger.cs b/src/Octoshift/Services/OctoLogger.cs index d3ad02508..4d310919e 100644 --- a/src/Octoshift/Services/OctoLogger.cs +++ b/src/Octoshift/Services/OctoLogger.cs @@ -23,6 +23,7 @@ public class OctoLogger private readonly string _logFilePath; private readonly string _verboseFilePath; private readonly bool _debugMode; + private readonly object _mutex = new(); private readonly Action _writeToLog; private readonly Action _writeToVerboseLog; @@ -103,20 +104,32 @@ private string Redact(string msg) return result; } - public virtual void LogInformation(string msg) => Log(msg, LogLevel.INFO); + public virtual void LogInformation(string msg) + { + lock (_mutex) + { + Log(msg, LogLevel.INFO); + } + } public virtual void LogWarning(string msg) { - Console.ForegroundColor = ConsoleColor.Yellow; - Log(msg, LogLevel.WARNING); - Console.ResetColor(); + lock (_mutex) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Log(msg, LogLevel.WARNING); + Console.ResetColor(); + } } public virtual void LogError(string msg) { - Console.ForegroundColor = ConsoleColor.Red; - Log(msg, LogLevel.ERROR); - Console.ResetColor(); + lock (_mutex) + { + Console.ForegroundColor = ConsoleColor.Red; + Log(msg, LogLevel.ERROR); + Console.ResetColor(); + } } public virtual void LogError(Exception ex) @@ -126,30 +139,36 @@ public virtual void LogError(Exception ex) throw new ArgumentNullException(nameof(ex)); } - var verboseMessage = ex is HttpRequestException httpEx ? $"[HTTP ERROR {(int?)httpEx.StatusCode}] {ex}" : ex.ToString(); - var logMessage = Verbose ? verboseMessage : ex is OctoshiftCliException ? ex.Message : GENERIC_ERROR_MESSAGE; + lock (_mutex) + { + var verboseMessage = ex is HttpRequestException httpEx ? $"[HTTP ERROR {(int?)httpEx.StatusCode}] {ex}" : ex.ToString(); + var logMessage = Verbose ? verboseMessage : ex is OctoshiftCliException ? ex.Message : GENERIC_ERROR_MESSAGE; - var output = Redact(FormatMessage(logMessage, LogLevel.ERROR)); + var output = Redact(FormatMessage(logMessage, LogLevel.ERROR)); - Console.ForegroundColor = ConsoleColor.Red; - _writeToConsoleError(output); - Console.ResetColor(); + Console.ForegroundColor = ConsoleColor.Red; + _writeToConsoleError(output); + Console.ResetColor(); - _writeToLog(output); - _writeToVerboseLog(Redact(FormatMessage(verboseMessage, LogLevel.ERROR))); + _writeToLog(output); + _writeToVerboseLog(Redact(FormatMessage(verboseMessage, LogLevel.ERROR))); + } } public virtual void LogVerbose(string msg) { - if (Verbose) + lock (_mutex) { - Console.ForegroundColor = ConsoleColor.Gray; - Log(msg, LogLevel.VERBOSE); - Console.ResetColor(); - } - else - { - _writeToVerboseLog(Redact(FormatMessage(msg, LogLevel.VERBOSE))); + if (Verbose) + { + Console.ForegroundColor = ConsoleColor.Gray; + Log(msg, LogLevel.VERBOSE); + Console.ResetColor(); + } + else + { + _writeToVerboseLog(Redact(FormatMessage(msg, LogLevel.VERBOSE))); + } } } @@ -163,9 +182,12 @@ public virtual void LogDebug(string msg) public virtual void LogSuccess(string msg) { - Console.ForegroundColor = ConsoleColor.Green; - Log(msg, LogLevel.SUCCESS); - Console.ResetColor(); + lock (_mutex) + { + Console.ForegroundColor = ConsoleColor.Green; + Log(msg, LogLevel.SUCCESS); + Console.ResetColor(); + } } public virtual void RegisterSecret(string secret) => _secrets.Add(secret);