diff --git a/src/Search.GenerateAuxiliaryData/BlobStorageExporter.cs b/src/Search.GenerateAuxiliaryData/BlobStorageExporter.cs index 59add4f2f..e13648f55 100644 --- a/src/Search.GenerateAuxiliaryData/BlobStorageExporter.cs +++ b/src/Search.GenerateAuxiliaryData/BlobStorageExporter.cs @@ -30,12 +30,12 @@ public BlobStorageExporter(ILogger logger, CloudBlobContainer sourceCo public override async Task ExportAsync() { - _logger.LogInformation("Copying {ReportName} report from {ConnectionString}/{SourceName}", _name, _sourceContainer.Uri, _sourceName); + _logger.LogInformation("Copying {ReportName} report from {ConnectionString}/{SourceName}", Name, _sourceContainer.Uri, _sourceName); await _destinationContainer.CreateIfNotExistsAsync(); var sourceCloudBlob = _sourceContainer.GetBlockBlobReference(_sourceName); - var destinationCloudBlob = _destinationContainer.GetBlockBlobReference(_name); + var destinationCloudBlob = _destinationContainer.GetBlockBlobReference(Name); await destinationCloudBlob.StartCopyAsync(sourceCloudBlob); @@ -57,7 +57,7 @@ public override async Task ExportAsync() throw new StorageException($"The blob copy operation had copy status {destinationCloudBlob.CopyState.Status} ({destinationCloudBlob.CopyState.StatusDescription})."); } - _logger.LogInformation("Copy of {ReportName} completed. Took: {Seconds} seconds.", _name, stopwatch.Elapsed.TotalSeconds); + _logger.LogInformation("Copy of {ReportName} completed. Took: {Seconds} seconds.", Name, stopwatch.Elapsed.TotalSeconds); } } } diff --git a/src/Search.GenerateAuxiliaryData/Exporter.cs b/src/Search.GenerateAuxiliaryData/Exporter.cs index af30dd0d9..adfefb0ad 100644 --- a/src/Search.GenerateAuxiliaryData/Exporter.cs +++ b/src/Search.GenerateAuxiliaryData/Exporter.cs @@ -11,17 +11,17 @@ namespace Search.GenerateAuxiliaryData // Public only to facilitate testing. public abstract class Exporter { - protected ILogger _logger; - protected CloudBlobContainer _destinationContainer; + protected readonly ILogger _logger; + protected readonly CloudBlobContainer _destinationContainer; - protected string _name { get; } + public string Name { get; } public Exporter(ILogger logger, CloudBlobContainer defaultDestinationContainer, string defaultName) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _destinationContainer = defaultDestinationContainer ?? throw new ArgumentNullException(nameof(defaultDestinationContainer)); - _name = defaultName; + Name = defaultName; } public abstract Task ExportAsync(); diff --git a/src/Search.GenerateAuxiliaryData/Job.cs b/src/Search.GenerateAuxiliaryData/Job.cs index 036a5fb9e..cf1744aff 100644 --- a/src/Search.GenerateAuxiliaryData/Job.cs +++ b/src/Search.GenerateAuxiliaryData/Job.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.Design; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Autofac; @@ -14,6 +15,7 @@ using Microsoft.WindowsAzure.Storage; using NuGet.Jobs; using NuGet.Jobs.Configuration; +using Search.GenerateAuxiliaryData.Telemetry; namespace Search.GenerateAuxiliaryData { @@ -37,12 +39,14 @@ public class Job : JsonConfigurationJob private List _exportersToRun; private InitializationConfiguration Configuration { get; set; } + public ITelemetryService TelemetryService { get; private set; } public override void Init(IServiceContainer serviceContainer, IDictionary jobArgsDictionary) { base.Init(serviceContainer, jobArgsDictionary); Configuration = _serviceProvider.GetRequiredService>().Value; + TelemetryService = _serviceProvider.GetRequiredService(); var destinationContainer = CloudStorageAccount.Parse(Configuration.PrimaryDestination) .CreateCloudBlobClient() @@ -94,16 +98,24 @@ public override async Task Run() foreach (Exporter exporter in _exportersToRun) { + var exporterName = exporter.GetType().Name; + var reportName = exporter.Name; + var stopwatch = Stopwatch.StartNew(); + var success = false; try { await exporter.ExportAsync(); + success = true; } catch (Exception e) { - var exporterName = exporter.GetType().Name; Logger.LogError("SQL exporter '{ExporterName}' failed: {Exception}", exporterName, e); failedExporters.Add(exporterName); } + finally + { + TelemetryService.TrackExporterDuration(exporterName, reportName, stopwatch.Elapsed, success); + } } if (failedExporters.Any()) @@ -119,6 +131,8 @@ protected override void ConfigureAutofacServices(ContainerBuilder containerBuild protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot) { ConfigureInitializationSection(services, configurationRoot); + + services.AddTransient(); } } } \ No newline at end of file diff --git a/src/Search.GenerateAuxiliaryData/Search.GenerateAuxiliaryData.csproj b/src/Search.GenerateAuxiliaryData/Search.GenerateAuxiliaryData.csproj index bb4dab907..b73a3e1fc 100644 --- a/src/Search.GenerateAuxiliaryData/Search.GenerateAuxiliaryData.csproj +++ b/src/Search.GenerateAuxiliaryData/Search.GenerateAuxiliaryData.csproj @@ -52,6 +52,8 @@ + + diff --git a/src/Search.GenerateAuxiliaryData/SqlExporter.cs b/src/Search.GenerateAuxiliaryData/SqlExporter.cs index 9ec40c83e..3bb01db07 100644 --- a/src/Search.GenerateAuxiliaryData/SqlExporter.cs +++ b/src/Search.GenerateAuxiliaryData/SqlExporter.cs @@ -34,7 +34,6 @@ public SqlExporter( TimeSpan commandTimeout) : base(logger, defaultDestinationContainer, defaultName) { - _logger = logger; OpenSqlConnectionAsync = openSqlConnectionAsync; _commandTimeout = commandTimeout; } @@ -60,12 +59,12 @@ public override async Task ExportAsync() using (var connection = await OpenSqlConnectionAsync()) { _logger.LogInformation("Generating {ReportName} report from {DataSource}/{InitialCatalog}.", - _name, connection.DataSource, connection.Database); + Name, connection.DataSource, connection.Database); result = GetResultOfQuery(connection); } - await WriteToBlobAsync(_logger, _destinationContainer, result.ToString(Formatting.None), _name); + await WriteToBlobAsync(_logger, _destinationContainer, result.ToString(Formatting.None), Name); } protected abstract JContainer GetResultOfQuery(SqlConnection connection); diff --git a/src/Search.GenerateAuxiliaryData/Telemetry/ITelemetryService.cs b/src/Search.GenerateAuxiliaryData/Telemetry/ITelemetryService.cs new file mode 100644 index 000000000..55f389884 --- /dev/null +++ b/src/Search.GenerateAuxiliaryData/Telemetry/ITelemetryService.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Search.GenerateAuxiliaryData.Telemetry +{ + public interface ITelemetryService + { + void TrackExporterDuration(string exporter, string report, TimeSpan duration, bool success); + } +} diff --git a/src/Search.GenerateAuxiliaryData/Telemetry/TelemetryService.cs b/src/Search.GenerateAuxiliaryData/Telemetry/TelemetryService.cs new file mode 100644 index 000000000..830bd7a93 --- /dev/null +++ b/src/Search.GenerateAuxiliaryData/Telemetry/TelemetryService.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using NuGet.Services.Logging; + +namespace Search.GenerateAuxiliaryData.Telemetry +{ + public class TelemetryService : ITelemetryService + { + private const string Prefix = "Search.GenerateAuxiliaryData."; + + private readonly ITelemetryClient _telemetryClient; + + public TelemetryService(ITelemetryClient telemetryClient) + { + _telemetryClient = telemetryClient; + } + + public void TrackExporterDuration(string exporter, string report, TimeSpan duration, bool success) + { + _telemetryClient.TrackMetric( + Prefix + "ExporterDurationMs", + duration.TotalMilliseconds, + new Dictionary + { + { "Exporter", exporter }, + { "Report", report }, + { "Success", success.ToString() }, + }); + } + } +} diff --git a/tests/Tests.Search.GenerateAuxiliaryData/Telemetry/TelemetryServiceFacts.cs b/tests/Tests.Search.GenerateAuxiliaryData/Telemetry/TelemetryServiceFacts.cs new file mode 100644 index 000000000..3fe7b3354 --- /dev/null +++ b/tests/Tests.Search.GenerateAuxiliaryData/Telemetry/TelemetryServiceFacts.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Moq; +using NuGet.Services.Logging; +using Search.GenerateAuxiliaryData.Telemetry; +using Xunit; + +namespace Tests.Search.GenerateAuxiliaryData.Telemetry +{ + public class TelemetryServiceFacts + { + public class TrackExporterDuration : BaseFacts + { + [Fact] + public void EmitsExpectedMetric() + { + Target.TrackExporterDuration( + "TheExporter", + "TheReport", + TimeSpan.FromMilliseconds(1234), + success: true); + + TelemetryClient.Verify( + x => x.TrackMetric( + "Search.GenerateAuxiliaryData.ExporterDurationMs", + 1234, + It.IsAny>()), + Times.Once); + + var properties = Assert.Single(Properties); + Assert.Equal(new[] { "Exporter", "Report", "Success" }, properties.Keys.OrderBy(x => x).ToArray()); + Assert.Equal("TheExporter", properties["Exporter"]); + Assert.Equal("TheReport", properties["Report"]); + Assert.Equal("True", properties["Success"]); + } + } + + public abstract class BaseFacts + { + public BaseFacts() + { + TelemetryClient = new Mock(); + Properties = new ConcurrentQueue>(); + + TelemetryClient + .Setup(x => x.TrackMetric(It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((_, __, p) => Properties.Enqueue(p)); + + Target = new TelemetryService(TelemetryClient.Object); + } + + public Mock TelemetryClient { get; } + public ConcurrentQueue> Properties { get; } + public TelemetryService Target { get; } + } + } +} diff --git a/tests/Tests.Search.GenerateAuxiliaryData/Tests.Search.GenerateAuxiliaryData.csproj b/tests/Tests.Search.GenerateAuxiliaryData/Tests.Search.GenerateAuxiliaryData.csproj index d10996431..cc00843e1 100644 --- a/tests/Tests.Search.GenerateAuxiliaryData/Tests.Search.GenerateAuxiliaryData.csproj +++ b/tests/Tests.Search.GenerateAuxiliaryData/Tests.Search.GenerateAuxiliaryData.csproj @@ -41,6 +41,7 @@ +