diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..48ed96152 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,136 @@ +############################### +# Core EditorConfig Options # +############################### +root = true +# All files +[*] +indent_style = space + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true + +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] +# Organize usings +dotnet_sort_system_directives_first = true +# this. preferences +dotnet_style_qualification_for_field = true:suggestion +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent + +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +# Use no prefix for private fields +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = suggestion +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_style.prefix_underscore.capitalization = camel_case + +############################### +# C# Coding Conventions # +############################### +[*.cs] +# var preferences +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = true:silent +# Expression-bodied members +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +# Expression-level preferences +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Azure.Sdk.Tools.PipelineWitness.csproj b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Azure.Sdk.Tools.PipelineWitness.csproj index 90948b848..252b4b6c5 100644 --- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Azure.Sdk.Tools.PipelineWitness.csproj +++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Azure.Sdk.Tools.PipelineWitness.csproj @@ -6,6 +6,7 @@ + diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/BlobUploadProcessor.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/BlobUploadProcessor.cs new file mode 100644 index 000000000..87ba3fc65 --- /dev/null +++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/BlobUploadProcessor.cs @@ -0,0 +1,118 @@ +namespace Azure.Sdk.Tools.PipelineWitness +{ + using System; + using System.IO; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + + using Azure.Sdk.Tools.PipelineWitness.Services; + using Azure.Storage.Blobs; + + using Microsoft.Extensions.Logging; + using Microsoft.TeamFoundation.Build.WebApi; + using Microsoft.VisualStudio.Services.WebApi; + + using Newtonsoft.Json; + using Newtonsoft.Json.Serialization; + + public class BlobUploadProcessor + { + private const string TimeFormat = @"yyyy-MM-dd\THH:mm:ss.fffffff\Z"; + private static readonly JsonSerializerSettings jsonSettings = new() + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Formatting = Formatting.None, + }; + + private readonly ILogger logger; + private readonly BuildLogProvider logProvider; + private readonly BlobContainerClient containerClient; + private readonly BuildHttpClient buildClient; + + public BlobUploadProcessor(ILogger logger, BuildLogProvider logProvider, BlobContainerClient containerClient, BuildHttpClient buildClient) + { + this.logger = logger; + this.logProvider = logProvider; + this.containerClient = containerClient; + this.buildClient = buildClient; + } + + public async Task UploadLogBlobsAsync(Build build) + { + var logs = await buildClient.GetBuildLogsAsync(build.Project.Id, build.Id); + + foreach (var log in logs) + { + var blobPath = $"{build.Project.Name}/{build.QueueTime:yyyy/MM/dd}/{build.Id}-{log.Id}.jsonl"; + var blobClient = containerClient.GetBlobClient(blobPath); + + if (await blobClient.ExistsAsync()) + { + this.logger.LogInformation("Skipping existing log {LogId} for build {BuildId}", log.Id, build.Id); + continue; + } + + await UploadLogBlobAsync(blobClient, build, log); + } + } + + private async Task UploadLogBlobAsync(BlobClient blobClient, Build build, BuildLog log) + { + var tempFile = Path.GetTempFileName(); + + try + { + await using (var messagesWriter = new StreamWriter(File.OpenWrite(tempFile))) + { + var logLines = await this.logProvider.GetLogLinesAsync(build, log.Id); + var lastTimeStamp = log.CreatedOn; + + for (var lineNumber = 1; lineNumber <= logLines.Count; lineNumber++) + { + var line = logLines[lineNumber - 1]; + var match = Regex.Match(line, @"^([^Z]{20,28}Z) (.*)$"); + var timestamp = match.Success + ? DateTime.ParseExact(match.Groups[1].Value, TimeFormat, null, + System.Globalization.DateTimeStyles.AssumeUniversal).ToUniversalTime() + : lastTimeStamp; + + var message = match.Success ? match.Groups[2].Value : line; + + if (timestamp == null) + { + throw new Exception($"Error processing line {lineNumber}. No leading timestamp."); + } + + await messagesWriter.WriteLineAsync(JsonConvert.SerializeObject( + new + { + OrganizationName = "azure-sdk", + ProjectId = build.Project.Id, + BuildId = build.Id, + BuildDefinitionId = build.Definition.Id, + LogId = log.Id, + LineNumber = lineNumber, + Length = message.Length, + Timestamp = timestamp?.ToString(TimeFormat), + Message = message, + EtlIngestDate = DateTime.UtcNow.ToString(TimeFormat), + }, jsonSettings)); + + lastTimeStamp = timestamp; + } + } + + await blobClient.UploadAsync(tempFile); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error processing log {LogId} for build {BuildId}", log.Id, build.Id); + throw; + } + finally + { + File.Delete(tempFile); + } + } + } +} diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/RunProcessor.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/RunProcessor.cs index ea0b003d7..65cca093d 100644 --- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/RunProcessor.cs +++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/RunProcessor.cs @@ -1,47 +1,40 @@ -using Azure.Cosmos; -using Azure.Identity; -using Azure.Sdk.Tools.PipelineWitness.Entities.AzurePipelines; -using Azure.Sdk.Tools.PipelineWitness.Services.FailureAnalysis; -using Azure.Security.KeyVault.Secrets; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Caching.Memory; +using System; +using System.Linq; +using System.Threading.Tasks; + +using Azure.Cosmos; + using Microsoft.Extensions.Logging; using Microsoft.TeamFoundation.Build.WebApi; using Microsoft.TeamFoundation.Core.WebApi; -using Microsoft.VisualStudio.Services.Common; -using Microsoft.VisualStudio.Services.Organization.Client; using Microsoft.VisualStudio.Services.WebApi; -using Microsoft.WindowsAzure.Storage.Blob.Protocol; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; + +using Azure.Sdk.Tools.PipelineWitness.Entities.AzurePipelines; +using Azure.Sdk.Tools.PipelineWitness.Services.FailureAnalysis; namespace Azure.Sdk.Tools.PipelineWitness { public class RunProcessor { - public RunProcessor(IFailureAnalyzer failureAnalyzer, ILogger logger, IMemoryCache cache, SecretClient secretClient, CosmosClient cosmosClient, VssConnection vssConnection) + public RunProcessor( + IFailureAnalyzer failureAnalyzer, + ILogger logger, + CosmosClient cosmosClient, + VssConnection vssConnection, + BlobUploadProcessor blobUploadProcessor) { this.failureAnalyzer = failureAnalyzer; this.logger = logger; - this.cache = cache; - this.secretClient = secretClient; + this.blobUploadProcessor = blobUploadProcessor; this.cosmosClient = cosmosClient; this.vssConnection = vssConnection; } - private IFailureAnalyzer failureAnalyzer; - private ILogger logger; - private IMemoryCache cache; - private SecretClient secretClient; - private CosmosClient cosmosClient; - private VssConnection vssConnection; + private readonly IFailureAnalyzer failureAnalyzer; + private readonly ILogger logger; + private readonly CosmosClient cosmosClient; + private readonly VssConnection vssConnection; + private readonly BlobUploadProcessor blobUploadProcessor; private bool IsValidAzureDevOpsUri(Uri uri) { @@ -55,7 +48,8 @@ namespace Azure.Sdk.Tools.PipelineWitness { if (!IsValidAzureDevOpsUri(runUri)) { - throw new ArgumentOutOfRangeException("Run URI does not point to the Azure SDK instance of Azure DevOps"); + throw new ArgumentOutOfRangeException( + "Run URI does not point to the Azure SDK instance of Azure DevOps"); } var runUriPath = runUri.AbsolutePath; @@ -67,8 +61,8 @@ namespace Azure.Sdk.Tools.PipelineWitness var buildClient = vssConnection.GetClient(); var projectClient = vssConnection.GetClient(); - var build = await buildClient.GetBuildAsync(projectGuid, runId); var project = await projectClient.GetProject(projectGuid.ToString()); + var build = await buildClient.GetBuildAsync(projectGuid, runId); var pipeline = await buildClient.GetDefinitionAsync(project.Id, build.Definition.Id); var timeline = await buildClient.GetBuildTimelineAsync(projectGuid, runId); @@ -78,20 +72,22 @@ namespace Azure.Sdk.Tools.PipelineWitness if (timeline != null) { agentDurationInSeconds = (from record in timeline.Records - where record.RecordType == "Task" - where (record.Name == "Initialize job" || record.Name == "Finalize Job") // Love consistency in capitalization! - group record by record.ParentId into job - let jobStartTime = job.Min(jobRecord => jobRecord.StartTime) - let jobFinishTime = job.Max(jobRecord => jobRecord.FinishTime) - let agentDuration = jobFinishTime - jobStartTime - select agentDuration.Value.TotalSeconds).Sum(); + where record.RecordType == "Task" + where (record.Name == "Initialize job" || + record.Name == "Finalize Job") // Love consistency in capitalization! + group record by record.ParentId + into job + let jobStartTime = job.Min(jobRecord => jobRecord.StartTime) + let jobFinishTime = job.Max(jobRecord => jobRecord.FinishTime) + let agentDuration = jobFinishTime - jobStartTime + select agentDuration.Value.TotalSeconds).Sum(); queueDurationInSeconds = (from taskRecord in timeline.Records - where taskRecord.RecordType == "Task" - where taskRecord.Name == "Initialize job" - join jobRecord in timeline.Records on taskRecord.ParentId equals jobRecord.Id - let queueDuration = taskRecord.StartTime - jobRecord.StartTime - select queueDuration.Value.TotalSeconds).Sum(); + where taskRecord.RecordType == "Task" + where taskRecord.Name == "Initialize job" + join jobRecord in timeline.Records on taskRecord.ParentId equals jobRecord.Id + let queueDuration = taskRecord.StartTime - jobRecord.StartTime + select queueDuration.Value.TotalSeconds).Sum(); } var run = new Run() @@ -144,6 +140,8 @@ namespace Azure.Sdk.Tools.PipelineWitness var container = await GetItemContainerAsync("azure-pipelines-runs"); await container.UpsertItemAsync(run); + + await this.blobUploadProcessor.UploadLogBlobsAsync(build); } public async Task GetFailureClassificationsAsync(Build build, Timeline timeline) @@ -152,6 +150,7 @@ namespace Azure.Sdk.Tools.PipelineWitness if (timeline == null) return new Failure[0]; var failures = await failureAnalyzer.AnalyzeFailureAsync(build, timeline); + return failures.ToArray(); } diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/BuildLogProvider.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/BuildLogProvider.cs new file mode 100644 index 000000000..efc907f0b --- /dev/null +++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/BuildLogProvider.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.TeamFoundation.Build.WebApi; +using Microsoft.VisualStudio.Services.WebApi; + +namespace Azure.Sdk.Tools.PipelineWitness.Services +{ + public class BuildLogProvider + { + private readonly ILogger logger; + private readonly IMemoryCache cache; + private readonly VssConnection vssConnection; + + public BuildLogProvider(ILogger logger, IMemoryCache cache, VssConnection vssConnection) + { + this.logger = logger; + this.cache = cache; + this.vssConnection = vssConnection; + } + + protected BuildLogProvider() + { + } + + public virtual async Task> GetLogLinesAsync(Build build, int logId) + { + var cacheKey = $"{build.Id}:{logId}"; + + logger.LogTrace("Getting logs for {CacheKey} from cache", cacheKey); + var lines = await this.cache.GetOrCreateAsync(cacheKey, async entry => + { + logger.LogTrace("Cache miss for {CacheKey}, falling back to rest api", cacheKey); + var buildHttpClient = vssConnection.GetClient(); + var response = await buildHttpClient.GetBuildLogLinesAsync(build.Project.Id, build.Id, logId); + var characterCount = response.Sum(x => x.Length); + entry.Priority = CacheItemPriority.Low; + entry.Size =characterCount + 4; + + logger.LogTrace("Caching {CharacterCount} characters in {LineCount} lines for {CacheKey}", characterCount, response.Count, cacheKey); + return response; + }); + + return lines; + } + } +} diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/AzureArtifactsServiceUnavailableClassifier.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/AzureArtifactsServiceUnavailableClassifier.cs index be75cfc59..c1768ef81 100644 --- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/AzureArtifactsServiceUnavailableClassifier.cs +++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/AzureArtifactsServiceUnavailableClassifier.cs @@ -1,24 +1,17 @@ -using Azure.Sdk.Tools.PipelineWitness.Entities.AzurePipelines; -using Microsoft.TeamFoundation.Build.WebApi; -using Microsoft.VisualStudio.Services.WebApi; -using System; -using System.Collections.Generic; +using Microsoft.TeamFoundation.Build.WebApi; using System.Linq; -using System.Text; using System.Threading.Tasks; namespace Azure.Sdk.Tools.PipelineWitness.Services.FailureAnalysis { public class AzureArtifactsServiceUnavailableClassifier : IFailureClassifier { - public AzureArtifactsServiceUnavailableClassifier(VssConnection vssConnection) + public AzureArtifactsServiceUnavailableClassifier(BuildLogProvider buildLogProvider) { - this.vssConnection = vssConnection; - buildClient = vssConnection.GetClient(); + this.buildLogProvider = buildLogProvider; } - private VssConnection vssConnection; - private BuildHttpClient buildClient; + private readonly BuildLogProvider buildLogProvider; public async Task ClassifyAsync(FailureAnalyzerContext context) { @@ -32,11 +25,7 @@ namespace Azure.Sdk.Tools.PipelineWitness.Services.FailureAnalysis foreach (var failedTask in failedTasks) { - var lines = await buildClient.GetBuildLogLinesAsync( - context.Build.Project.Id, - context.Build.Id, - failedTask.Log.Id - ); + var lines = await this.buildLogProvider.GetLogLinesAsync(context.Build, failedTask.Log.Id); if (lines.Any(line => line.Contains("Transfer failed for https://pkgs.dev.azure.com") && line.Contains("503 Service Unavailable"))) { diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/DnsResolutionFailureClassifier.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/DnsResolutionFailureClassifier.cs index 232e78cb0..39684f3ab 100644 --- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/DnsResolutionFailureClassifier.cs +++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/DnsResolutionFailureClassifier.cs @@ -11,14 +11,12 @@ namespace Azure.Sdk.Tools.PipelineWitness.Services.FailureAnalysis { public class DnsResolutionFailureClassifier : IFailureClassifier { - public DnsResolutionFailureClassifier(VssConnection vssConnection) + public DnsResolutionFailureClassifier(BuildLogProvider buildLogProvider) { - this.vssConnection = vssConnection; - buildClient = vssConnection.GetClient(); + this.buildLogProvider = buildLogProvider; } - private VssConnection vssConnection; - private BuildHttpClient buildClient; + private readonly BuildLogProvider buildLogProvider; private bool IsDnsResolutionFailure(string line) { @@ -39,11 +37,7 @@ namespace Azure.Sdk.Tools.PipelineWitness.Services.FailureAnalysis foreach (var failedTask in failedTasks) { - var lines = await buildClient.GetBuildLogLinesAsync( - context.Build.Project.Id, - context.Build.Id, - failedTask.Log.Id - ); + var lines = await buildLogProvider.GetLogLinesAsync(context.Build, failedTask.Log.Id); if (lines.Any(line => IsDnsResolutionFailure(line))) { diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/MavenBrokenPipeFailureClassifier.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/MavenBrokenPipeFailureClassifier.cs index 9a1ae072a..5e7441d41 100644 --- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/MavenBrokenPipeFailureClassifier.cs +++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Services/FailureAnalysis/MavenBrokenPipeFailureClassifier.cs @@ -11,14 +11,12 @@ namespace Azure.Sdk.Tools.PipelineWitness.Services.FailureAnalysis { public class MavenBrokenPipeFailureClassifier : IFailureClassifier { - public MavenBrokenPipeFailureClassifier(VssConnection vssConnection) + public MavenBrokenPipeFailureClassifier(BuildLogProvider buildLogProvider) { - this.vssConnection = vssConnection; - buildClient = vssConnection.GetClient(); + this.buildLogProvider = buildLogProvider; } - private VssConnection vssConnection; - private BuildHttpClient buildClient; + private readonly BuildLogProvider buildLogProvider; public async Task ClassifyAsync(FailureAnalyzerContext context) { @@ -32,11 +30,7 @@ namespace Azure.Sdk.Tools.PipelineWitness.Services.FailureAnalysis foreach (var failedTask in failedTasks) { - var lines = await buildClient.GetBuildLogLinesAsync( - context.Build.Project.Id, - context.Build.Id, - failedTask.Log.Id - ); + var lines = await buildLogProvider.GetLogLinesAsync(context.Build, failedTask.Log.Id); if (lines.Any(line => line.Contains("Connection reset") || line.Contains("Connection timed out") || line.Contains("504 Gateway Timeout"))) { diff --git a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Startup.cs b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Startup.cs index 969213911..31dca7fcb 100644 --- a/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Startup.cs +++ b/tools/pipeline-witness/Azure.Sdk.Tools.PipelineWitness/Startup.cs @@ -17,6 +17,9 @@ using System.Text; namespace Azure.Sdk.Tools.PipelineWitness { + using Microsoft.TeamFoundation.Build.WebApi; + using Microsoft.TeamFoundation.Core.WebApi; + public class Startup : FunctionsStartup { private string GetWebsiteResourceGroupEnvironmentVariable() @@ -25,14 +28,23 @@ namespace Azure.Sdk.Tools.PipelineWitness return websiteResourceGroupEnvironmentVariable; } + private string GetBuildBlobStorageEnvironmentVariable() + { + var environmentVariable = Environment.GetEnvironmentVariable("BUILD_BLOB_STORAGE_URI"); + return environmentVariable; + } + public override void Configure(IFunctionsHostBuilder builder) { var websiteResourceGroupEnvironmentVariable = GetWebsiteResourceGroupEnvironmentVariable(); + var buildBlobStorageUri = GetBuildBlobStorageEnvironmentVariable(); builder.Services.AddAzureClients(builder => { var keyVaultUri = new Uri($"https://{websiteResourceGroupEnvironmentVariable}.vault.azure.net/"); builder.AddSecretClient(keyVaultUri); + + builder.AddBlobServiceClient(new Uri(buildBlobStorageUri)); }); builder.Services.AddSingleton(provider => @@ -69,6 +81,9 @@ namespace Azure.Sdk.Tools.PipelineWitness return connection; }); + builder.Services.AddSingleton(provider => provider.GetRequiredService().GetClient()); + builder.Services.AddSingleton(provider => provider.GetRequiredService().GetClient()); + builder.Services.AddLogging(); builder.Services.AddMemoryCache(); builder.Services.AddSingleton(); diff --git a/tools/pipeline-witness/PipelineWitness.sln b/tools/pipeline-witness/PipelineWitness.sln index 40baa8d7d..c93dd78ad 100644 --- a/tools/pipeline-witness/PipelineWitness.sln +++ b/tools/pipeline-witness/PipelineWitness.sln @@ -8,6 +8,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Sdk.Tools.PipelineWitness.Tests", "Azure.Sdk.Tools.PipelineWitness.Tests\Azure.Sdk.Tools.PipelineWitness.Tests.csproj", "{AE649A76-2DDA-4B45-A426-21425A0B988A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FD213B4A-A8B5-400D-ABD3-1D5B1551CE3C}" +ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig +EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution