Skip to content

Commit a439041

Browse files
authored
Merge pull request #10566 from NuGet/agr-whr
Restored Stats.CreateAzureCdnWarehoseReports job, migrated to new SDK, added MSI support
2 parents 8feb891 + 4360529 commit a439041

17 files changed

Lines changed: 515 additions & 0 deletions

NuGet.Jobs.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Stats.Warehouse", "src\Stat
5454
EndProject
5555
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Statistics", "Statistics", "{B9D03824-A9CA-43AC-86D6-8BB399B9A228}"
5656
EndProject
57+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stats.CreateAzureCdnWarehouseReports", "src\Stats.CreateAzureCdnWarehouseReports\Stats.CreateAzureCdnWarehouseReports.csproj", "{E9D937CE-7D3D-4B4A-8FD1-AF721F362948}"
58+
EndProject
5759
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stats.AggregateCdnDownloadsInGallery", "src\Stats.AggregateCdnDownloadsInGallery\Stats.AggregateCdnDownloadsInGallery.csproj", "{6E27275F-0A0B-BB64-4C28-8F3E894B8C9E}"
5860
EndProject
5961
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Validation", "Validation", "{678D7B14-F8BC-4193-99AF-2EE8AA390A02}"
@@ -333,6 +335,10 @@ Global
333335
{21846E0D-0E79-4E43-9D92-E78854370994}.Release|Any CPU.ActiveCfg = Release|Any CPU
334336
{21846E0D-0E79-4E43-9D92-E78854370994}.Release|Any CPU.Build.0 = Release|Any CPU
335337
{21846E0D-0E79-4E43-9D92-E78854370994}.Release|Any CPU.Deploy.0 = Release|Any CPU
338+
{E9D937CE-7D3D-4B4A-8FD1-AF721F362948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
339+
{E9D937CE-7D3D-4B4A-8FD1-AF721F362948}.Debug|Any CPU.Build.0 = Debug|Any CPU
340+
{E9D937CE-7D3D-4B4A-8FD1-AF721F362948}.Release|Any CPU.ActiveCfg = Release|Any CPU
341+
{E9D937CE-7D3D-4B4A-8FD1-AF721F362948}.Release|Any CPU.Build.0 = Release|Any CPU
336342
{6E27275F-0A0B-BB64-4C28-8F3E894B8C9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
337343
{6E27275F-0A0B-BB64-4C28-8F3E894B8C9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
338344
{6E27275F-0A0B-BB64-4C28-8F3E894B8C9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -653,6 +659,7 @@ Global
653659
{CCB4D5EF-AC84-449D-AC6E-0A0AD295483A} = {6A776396-02B1-475D-A104-26940ADB04AB}
654660
{F72C31A7-424D-48C6-924C-EBFD4BE0918B} = {B9D03824-A9CA-43AC-86D6-8BB399B9A228}
655661
{21846E0D-0E79-4E43-9D92-E78854370994} = {B9D03824-A9CA-43AC-86D6-8BB399B9A228}
662+
{E9D937CE-7D3D-4B4A-8FD1-AF721F362948} = {B9D03824-A9CA-43AC-86D6-8BB399B9A228}
656663
{6E27275F-0A0B-BB64-4C28-8F3E894B8C9E} = {B9D03824-A9CA-43AC-86D6-8BB399B9A228}
657664
{FA8C7905-985F-4919-AAA9-4B9A252F4977} = {88725659-D5F8-49F9-9B7E-D87C5B9917D7}
658665
{12719498-B87E-4E92-8C2B-30046393CF85} = {88725659-D5F8-49F9-9B7E-D87C5B9917D7}

build.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ Invoke-BuildStep 'Creating job packages from jobs solution' {
150150
"src\Stats.AggregateCdnDownloadsInGallery\Stats.AggregateCdnDownloadsInGallery.nuspec",
151151
"src\Stats.CDNLogsSanitizer\Stats.CDNLogsSanitizer.nuspec",
152152
"src\Stats.CollectAzureChinaCDNLogs\Stats.CollectAzureChinaCDNLogs.nuspec",
153+
"src\Stats.CreateAzureCdnWarehouseReports\Stats.CreateAzureCdnWarehouseReports.nuspec",
153154
"src\Stats.PostProcessReports\Stats.PostProcessReports.nuspec",
154155
"src\StatusAggregator\StatusAggregator.nuspec",
155156
"src\Validation.PackageSigning.ProcessSignature\Validation.PackageSigning.ProcessSignature.nuspec",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.ApplicationInsights;
6+
using Microsoft.ApplicationInsights.DataContracts;
7+
using Microsoft.ApplicationInsights.Extensibility;
8+
9+
namespace Stats.CreateAzureCdnWarehouseReports
10+
{
11+
internal sealed class ApplicationInsightsHelper
12+
{
13+
private readonly TelemetryClient _telemetryClient;
14+
15+
public ApplicationInsightsHelper(TelemetryConfiguration telemetryConfiguration)
16+
{
17+
if (telemetryConfiguration == null)
18+
{
19+
throw new ArgumentNullException(nameof(telemetryConfiguration));
20+
}
21+
22+
_telemetryClient = new TelemetryClient(telemetryConfiguration);
23+
}
24+
25+
public void TrackReportProcessed(string reportName)
26+
{
27+
var telemetry = new MetricTelemetry(reportName, 1);
28+
29+
_telemetryClient.TrackMetric(telemetry);
30+
_telemetryClient.Flush();
31+
}
32+
33+
public void TrackMetric(string metricName, double value)
34+
{
35+
var telemetry = new MetricTelemetry(metricName, value);
36+
37+
_telemetryClient.TrackMetric(telemetry);
38+
_telemetryClient.Flush();
39+
}
40+
}
41+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Stats.CreateAzureCdnWarehouseReports
5+
{
6+
public class CreateAzureCdnWarehouseReportsConfiguration
7+
{
8+
public string AzureCdnCloudStorageAccount { get; set; }
9+
10+
public string AzureCdnCloudStorageContainerName { get; set; }
11+
12+
public string AdditionalGalleryTotalsStorageAccount { get; set; }
13+
14+
public string AdditionalGalleryTotalsStorageContainerName { get; set; }
15+
16+
public int? CommandTimeOut { get; set; }
17+
}
18+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel.Design;
7+
using System.Diagnostics;
8+
using System.Threading.Tasks;
9+
using Autofac;
10+
using Azure.Core;
11+
using Azure.Storage.Blobs;
12+
using Microsoft.Extensions.Configuration;
13+
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.Extensions.Logging;
15+
using Microsoft.Extensions.Options;
16+
using NuGet.Jobs;
17+
using NuGet.Jobs.Configuration;
18+
19+
namespace Stats.CreateAzureCdnWarehouseReports
20+
{
21+
public class CreateAzureCdnWarehouseReportsJob : JsonConfigurationJob
22+
{
23+
private const int DefaultSqlCommandTimeoutSeconds = 1800; // 30 minute SQL command timeout by default
24+
25+
private BlobServiceClient _cloudStorageAccount;
26+
private BlobServiceClient _additionalGalleryTotalsAccount;
27+
private string _statisticsContainerName;
28+
private string _additionalGalleryTotalsContainerName;
29+
private int _sqlCommandTimeoutSeconds = DefaultSqlCommandTimeoutSeconds;
30+
private ApplicationInsightsHelper _applicationInsightsHelper;
31+
32+
public override void Init(IServiceContainer serviceContainer, IDictionary<string, string> jobArgsDictionary)
33+
{
34+
base.Init(serviceContainer, jobArgsDictionary);
35+
36+
var configuration = _serviceProvider.GetRequiredService<IOptionsSnapshot<CreateAzureCdnWarehouseReportsConfiguration>>().Value;
37+
38+
_sqlCommandTimeoutSeconds = configuration.CommandTimeOut ?? DefaultSqlCommandTimeoutSeconds;
39+
40+
_cloudStorageAccount = ValidateAzureCloudStorageAccount(
41+
configuration.AzureCdnCloudStorageAccount,
42+
nameof(configuration.AzureCdnCloudStorageAccount),
43+
_serviceProvider);
44+
45+
_statisticsContainerName = ValidateAzureContainerName(
46+
configuration.AzureCdnCloudStorageContainerName,
47+
nameof(configuration.AzureCdnCloudStorageContainerName));
48+
49+
if (!string.IsNullOrWhiteSpace(configuration.AdditionalGalleryTotalsStorageAccount))
50+
{
51+
_additionalGalleryTotalsAccount = ValidateAzureCloudStorageAccount(
52+
configuration.AdditionalGalleryTotalsStorageAccount,
53+
nameof(configuration.AdditionalGalleryTotalsStorageAccount),
54+
_serviceProvider);
55+
Logger.LogInformation("Additional totals account found {BlobEndpoint}", _additionalGalleryTotalsAccount.Uri.GetLeftPart(UriPartial.Path));
56+
57+
_additionalGalleryTotalsContainerName = configuration.AdditionalGalleryTotalsStorageContainerName;
58+
}
59+
60+
_applicationInsightsHelper = new ApplicationInsightsHelper(ApplicationInsightsConfiguration.TelemetryConfiguration);
61+
}
62+
63+
public override async Task Run()
64+
{
65+
var reportGenerationTime = DateTime.UtcNow;
66+
var destinationContainer = _cloudStorageAccount.GetBlobContainerClient(_statisticsContainerName);
67+
68+
Logger.LogInformation("Generating reports and saving to {AccountName}/{Container}",
69+
_cloudStorageAccount.AccountName, destinationContainer.Name);
70+
71+
// build stats-totals.json
72+
var stopwatch = Stopwatch.StartNew();
73+
74+
var targets = new List<StorageContainerTarget>
75+
{
76+
new(_cloudStorageAccount, _statisticsContainerName)
77+
};
78+
if (_additionalGalleryTotalsAccount != null && !string.IsNullOrWhiteSpace(_additionalGalleryTotalsContainerName))
79+
{
80+
targets.Add(new StorageContainerTarget(_additionalGalleryTotalsAccount, _additionalGalleryTotalsContainerName));
81+
Logger.LogInformation("Added additional target for stats totals report {BlobEndpoint}/{Container}",
82+
_additionalGalleryTotalsAccount.Uri.GetLeftPart(UriPartial.Path),
83+
_additionalGalleryTotalsContainerName);
84+
}
85+
var galleryTotalsReport = new GalleryTotalsReport(
86+
LoggerFactory.CreateLogger<GalleryTotalsReport>(),
87+
targets,
88+
OpenSqlConnectionAsync<GalleryDbConfiguration>,
89+
commandTimeoutSeconds: _sqlCommandTimeoutSeconds);
90+
await galleryTotalsReport.Run();
91+
92+
stopwatch.Stop();
93+
var reportMetricName = ReportNames.GalleryTotals + ReportNames.Extension;
94+
_applicationInsightsHelper.TrackMetric(reportMetricName + " Generation Time (ms)", stopwatch.ElapsedMilliseconds);
95+
_applicationInsightsHelper.TrackReportProcessed(reportMetricName);
96+
}
97+
98+
private static BlobServiceClient ValidateAzureCloudStorageAccount(
99+
string cloudStorageAccount,
100+
string configurationName,
101+
IServiceProvider serviceProvider)
102+
{
103+
if (string.IsNullOrEmpty(cloudStorageAccount))
104+
{
105+
throw new ArgumentException($"Job configuration {configurationName} is not defined.");
106+
}
107+
108+
var storageMsiConfiguration = serviceProvider.GetRequiredService<IOptions<StorageMsiConfiguration>>().Value;
109+
110+
var blobClientOptions = new BlobClientOptions()
111+
{
112+
Retry =
113+
{
114+
Delay = TimeSpan.FromSeconds(10),
115+
MaxRetries = 5,
116+
Mode = RetryMode.Exponential,
117+
NetworkTimeout = TimeSpan.FromSeconds(30)
118+
}
119+
};
120+
121+
if (storageMsiConfiguration is null || !storageMsiConfiguration.UseManagedIdentity)
122+
{
123+
// using connection string
124+
return new BlobServiceClient(cloudStorageAccount, blobClientOptions);
125+
}
126+
127+
// using token credential with blob endpoint
128+
var tempClient = new BlobServiceClient(cloudStorageAccount);
129+
var blobEndpoint = new Uri(tempClient.Uri.GetLeftPart(UriPartial.Path));
130+
131+
var tokenCredential = serviceProvider.GetRequiredService<TokenCredential>();
132+
return new BlobServiceClient(blobEndpoint, tokenCredential, blobClientOptions);
133+
}
134+
135+
private static string ValidateAzureContainerName(string containerName, string configurationName)
136+
{
137+
if (string.IsNullOrWhiteSpace(containerName))
138+
{
139+
throw new ArgumentException($"Job configuration {configurationName} is not defined.");
140+
}
141+
142+
return containerName;
143+
}
144+
145+
protected override void ConfigureAutofacServices(ContainerBuilder containerBuilder, IConfigurationRoot configurationRoot)
146+
{
147+
}
148+
149+
protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot)
150+
{
151+
ConfigureInitializationSection<CreateAzureCdnWarehouseReportsConfiguration>(services, configurationRoot);
152+
services.ConfigureStorageMsi(configurationRoot);
153+
}
154+
}
155+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Newtonsoft.Json;
6+
7+
namespace Stats.CreateAzureCdnWarehouseReports
8+
{
9+
public class GalleryTotalsData
10+
{
11+
[JsonProperty("downloads")]
12+
public long Downloads { get; set; }
13+
14+
[JsonProperty("uniquePackages")]
15+
public int UniquePackages { get; set; }
16+
17+
[JsonProperty("totalPackages")]
18+
public int TotalPackages { get; set; }
19+
20+
[JsonProperty("lastUpdateDateUtc")]
21+
public DateTime? LastUpdateDateUtc { get; set; }
22+
}
23+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Data;
7+
using System.Data.SqlClient;
8+
using System.Linq;
9+
using System.Threading.Tasks;
10+
using Azure.Storage.Blobs.Models;
11+
using Microsoft.Extensions.Logging;
12+
using Newtonsoft.Json;
13+
14+
namespace Stats.CreateAzureCdnWarehouseReports
15+
{
16+
public class GalleryTotalsReport
17+
: ReportBase
18+
{
19+
private const string GalleryQuery = @"SELECT
20+
(SELECT COUNT(DISTINCT [PackageRegistrationKey]) FROM Packages p WITH (NOLOCK)
21+
WHERE p.Listed = 1 AND p.Deleted = 0) AS UniquePackages,
22+
(SELECT COUNT([Key]) FROM Packages WITH (NOLOCK) WHERE Listed = 1 AND Deleted = 0) AS TotalPackages";
23+
24+
public GalleryTotalsReport(
25+
ILogger<GalleryTotalsReport> logger,
26+
ICollection<StorageContainerTarget> targets,
27+
Func<Task<SqlConnection>> openGallerySqlConnectionAsync,
28+
int commandTimeoutSeconds)
29+
: base(
30+
logger,
31+
targets,
32+
openGallerySqlConnectionAsync: openGallerySqlConnectionAsync,
33+
commandTimeoutSeconds: commandTimeoutSeconds)
34+
{
35+
}
36+
37+
public async Task Run()
38+
{
39+
// gather package numbers from gallery database
40+
GalleryTotalsData totalsData;
41+
42+
using (var connection = await _openGallerySqlConnectionAsync())
43+
using (var transaction = connection.BeginTransaction(IsolationLevel.Snapshot))
44+
{
45+
_logger.LogInformation("Gathering Gallery Totals from {GalleryDataSource}/{GalleryInitialCatalog}...",
46+
connection.DataSource, connection.Database);
47+
48+
totalsData = (await connection.QueryWithRetryAsync<GalleryTotalsData>(
49+
GalleryQuery, commandType: CommandType.Text, transaction: transaction)).First();
50+
}
51+
52+
_logger.LogInformation("Total packages: {TotalPackagesCount}", totalsData.TotalPackages);
53+
_logger.LogInformation("Unique packages: {UniquePackagesCount}", totalsData.UniquePackages);
54+
55+
// write to blob
56+
totalsData.LastUpdateDateUtc = DateTime.UtcNow;
57+
58+
var reportText = JsonConvert.SerializeObject(totalsData);
59+
60+
foreach (var storageContainerTarget in _targets)
61+
{
62+
try
63+
{
64+
var targetBlobContainer = await GetBlobContainer(storageContainerTarget);
65+
var blob = targetBlobContainer.GetBlobClient(ReportNames.GalleryTotals + ReportNames.Extension);
66+
_logger.LogInformation("Writing report to {ReportUri}", blob.Uri.GetLeftPart(UriPartial.Path));
67+
var content = new BinaryData(reportText);
68+
var options = new BlobUploadOptions
69+
{
70+
HttpHeaders = new BlobHttpHeaders { ContentType = "application/json" }
71+
};
72+
await blob.UploadAsync(content, options);
73+
_logger.LogInformation("Wrote report to {ReportUri}", blob.Uri.GetLeftPart(UriPartial.Path));
74+
}
75+
catch (Exception ex)
76+
{
77+
_logger.LogError(ex, "Error writing report to storage account {StorageAccount}, container {ReportContainer}. {Exception}",
78+
storageContainerTarget.StorageAccount.AccountName,
79+
storageContainerTarget.ContainerName,
80+
ex);
81+
}
82+
}
83+
}
84+
}
85+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using NuGet.Jobs;
5+
6+
namespace Stats.CreateAzureCdnWarehouseReports
7+
{
8+
class Program
9+
{
10+
static void Main(string[] args)
11+
{
12+
var job = new CreateAzureCdnWarehouseReportsJob();
13+
JobRunner.Run(job, args).Wait();
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)