diff --git a/aspnetcore/openshift/indexer/template-indexer-devel.yml b/aspnetcore/openshift/indexer/template-indexer-devel.yml index 723d511..e4bf922 100644 --- a/aspnetcore/openshift/indexer/template-indexer-devel.yml +++ b/aspnetcore/openshift/indexer/template-indexer-devel.yml @@ -95,6 +95,21 @@ objects: configMapKeyRef: name: publicapi-api-config-devel key: "Serilog__WriteTo__HttpSink__Args__requestUri" + - name: "Serilog__Properties__WoodLogProjectNumber" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-devel + key: "Serilog__Properties__WoodLogProjectNumber" + - name: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-devel + key: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + - name: "Serilog__Properties__WoodLogUsecaseIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-devel + key: "Serilog__Properties__WoodLogUsecaseIndexer" - name: "QueryTimeout" valueFrom: configMapKeyRef: diff --git a/aspnetcore/openshift/indexer/template-indexer-manualjob-devel.yml b/aspnetcore/openshift/indexer/template-indexer-manualjob-devel.yml index 241d5d0..70b80f8 100644 --- a/aspnetcore/openshift/indexer/template-indexer-manualjob-devel.yml +++ b/aspnetcore/openshift/indexer/template-indexer-manualjob-devel.yml @@ -1,6 +1,6 @@ # This file is part of the research.fi api # -# Copyright 2022 Ministry of Education and Culture, Finland +# Copyright 2024 Ministry of Education and Culture, Finland # # :author: CSC - IT Center for Science Ltd., Espoo Finland servicedesk@csc.fi # :license: MIT @@ -90,6 +90,21 @@ objects: configMapKeyRef: name: publicapi-api-config-devel key: "Serilog__WriteTo__HttpSink__Args__requestUri" + - name: "Serilog__Properties__WoodLogProjectNumber" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-devel + key: "Serilog__Properties__WoodLogProjectNumber" + - name: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-devel + key: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + - name: "Serilog__Properties__WoodLogUsecaseIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-devel + key: "Serilog__Properties__WoodLogUsecaseIndexer" - name: "QueryTimeout" valueFrom: configMapKeyRef: diff --git a/aspnetcore/openshift/indexer/template-indexer-manualjob-production.yml b/aspnetcore/openshift/indexer/template-indexer-manualjob-production.yml index 6ccd9e6..3667fbd 100644 --- a/aspnetcore/openshift/indexer/template-indexer-manualjob-production.yml +++ b/aspnetcore/openshift/indexer/template-indexer-manualjob-production.yml @@ -90,6 +90,21 @@ objects: configMapKeyRef: name: publicapi-api-config-production key: "Serilog__WriteTo__HttpSink__Args__requestUri" + - name: "Serilog__Properties__WoodLogProjectNumber" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-production + key: "Serilog__Properties__WoodLogProjectNumber" + - name: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-production + key: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + - name: "Serilog__Properties__WoodLogUsecaseIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-production + key: "Serilog__Properties__WoodLogUsecaseIndexer" - name: "QueryTimeout" valueFrom: configMapKeyRef: diff --git a/aspnetcore/openshift/indexer/template-indexer-manualjob-qa.yml b/aspnetcore/openshift/indexer/template-indexer-manualjob-qa.yml index a227d0e..23ba631 100644 --- a/aspnetcore/openshift/indexer/template-indexer-manualjob-qa.yml +++ b/aspnetcore/openshift/indexer/template-indexer-manualjob-qa.yml @@ -90,6 +90,21 @@ objects: configMapKeyRef: name: publicapi-api-config-qa key: "Serilog__WriteTo__HttpSink__Args__requestUri" + - name: "Serilog__Properties__WoodLogProjectNumber" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-qa + key: "Serilog__Properties__WoodLogProjectNumber" + - name: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-qa + key: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + - name: "Serilog__Properties__WoodLogUsecaseIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-qa + key: "Serilog__Properties__WoodLogUsecaseIndexer" - name: "QueryTimeout" valueFrom: configMapKeyRef: diff --git a/aspnetcore/openshift/indexer/template-indexer-production.yml b/aspnetcore/openshift/indexer/template-indexer-production.yml index 393590b..b686ee0 100644 --- a/aspnetcore/openshift/indexer/template-indexer-production.yml +++ b/aspnetcore/openshift/indexer/template-indexer-production.yml @@ -95,6 +95,21 @@ objects: configMapKeyRef: name: publicapi-api-config-production key: "Serilog__WriteTo__HttpSink__Args__requestUri" + - name: "Serilog__Properties__WoodLogProjectNumber" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-production + key: "Serilog__Properties__WoodLogProjectNumber" + - name: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-production + key: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + - name: "Serilog__Properties__WoodLogUsecaseIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-production + key: "Serilog__Properties__WoodLogUsecaseIndexer" - name: "QueryTimeout" valueFrom: configMapKeyRef: diff --git a/aspnetcore/openshift/indexer/template-indexer-qa.yml b/aspnetcore/openshift/indexer/template-indexer-qa.yml index 868c629..13f02ef 100644 --- a/aspnetcore/openshift/indexer/template-indexer-qa.yml +++ b/aspnetcore/openshift/indexer/template-indexer-qa.yml @@ -95,6 +95,21 @@ objects: configMapKeyRef: name: publicapi-api-config-qa key: "Serilog__WriteTo__HttpSink__Args__requestUri" + - name: "Serilog__Properties__WoodLogProjectNumber" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-qa + key: "Serilog__Properties__WoodLogProjectNumber" + - name: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-qa + key: "Serilog__Properties__WoodLogRetentionMonthsIndexer" + - name: "Serilog__Properties__WoodLogUsecaseIndexer" + valueFrom: + configMapKeyRef: + name: publicapi-api-config-qa + key: "Serilog__Properties__WoodLogUsecaseIndexer" - name: "QueryTimeout" valueFrom: configMapKeyRef: diff --git a/aspnetcore/src/ElasticService/ElasticSearchIndexService.cs b/aspnetcore/src/ElasticService/ElasticSearchIndexService.cs index 7e4b48e..59a5e9e 100644 --- a/aspnetcore/src/ElasticService/ElasticSearchIndexService.cs +++ b/aspnetcore/src/ElasticService/ElasticSearchIndexService.cs @@ -34,9 +34,9 @@ public async Task IndexAsync(string indexName, List entities, Type model // Switch indexes await SwitchIndexes(indexName, indexToCreate, indexToDelete); - _logger.LogDebug("{EntityType}: Indexing to {IndexName} complete", modelType.Name, indexName); + _logger.LogDebug("{EntityType:l}: Indexing to {IndexName:l} complete", modelType.Name, indexName); } - + public async Task IndexChunkAsync(string indexToCreate, List entities, Type modelType) { // Add entities to the index. @@ -89,28 +89,27 @@ await _elasticClient.Indices.BulkAliasAsync(r => r private async Task IndexEntities(string indexName, List entities, Type modelType) where T : class { - var indexedCount = 0; - // Split entities into batches to avoid one big request. var documentBatches = new List>(); for (var docIndex = 0; docIndex < entities.Count; docIndex += BatchSize) { documentBatches.Add(entities.GetRange(docIndex, Math.Min(BatchSize, entities.Count - docIndex))); } - + + int batchCounter = 0; foreach (var batchToIndex in documentBatches) { + ++batchCounter; + _logger.LogInformation("{EntityType:l}: Indexing {ElasticsearchBatchSize} documents. Batch {ElasticsearchBatchCurrent}/{ElasticsearchBatchCount}", modelType.Name, batchToIndex.Count, batchCounter, documentBatches.Count); var indexBatchResponse = await _elasticClient.BulkAsync(b => b .Index(indexName) .IndexMany(batchToIndex)); if (!indexBatchResponse.IsValid) { - _logger.LogError(indexBatchResponse.OriginalException, "{EntityType}: Indexing documents to {IndexName} failed", modelType, indexName); + _logger.LogError("{EntityType:l}: Indexing documents to {IndexName:l} failed: {IndexerException}", modelType, indexName, indexBatchResponse.OriginalException.ToString()); throw new InvalidOperationException($"Indexing documents to {indexName} failed.", indexBatchResponse.OriginalException); } - indexedCount = indexedCount + batchToIndex.Count; - _logger.LogInformation("{EntityType}: Indexed {BatchSize} documents to {IndexName}", modelType.Name, batchToIndex.Count, indexName); } } @@ -136,6 +135,6 @@ await _elasticClient.Indices.DeleteAsync(indexName, throw new InvalidOperationException($"Creating index {indexName} failed.", createResponse.OriginalException); } - _logger.LogDebug("{EntityType}: Index {IndexName} created", type.Name, indexName); + _logger.LogDebug("{EntityType:l}: Index {IndexName:l} created", type.Name, indexName); } } \ No newline at end of file diff --git a/aspnetcore/src/Indexer/Indexer.cs b/aspnetcore/src/Indexer/Indexer.cs index bd7b21a..26f7e46 100644 --- a/aspnetcore/src/Indexer/Indexer.cs +++ b/aspnetcore/src/Indexer/Indexer.cs @@ -24,7 +24,7 @@ public Indexer( IElasticSearchIndexService indexService, IConfiguration configuration, IndexNameSettings indexNameSettings, - IEnumerable indexRepositories, + IEnumerable indexRepositories, IMemoryCache memoryCache) { _logger = logger; @@ -37,11 +37,11 @@ public Indexer( public async Task Start() { - Stopwatch stopWatch = new(); - stopWatch.Start(); + Stopwatch stopWatchMain = new(); + stopWatchMain.Start(); - _logger.LogInformation("Starting indexing..."); - _logger.LogInformation("Using ElasticSearch at '{ElasticSearchAddress}'", _configuration["ELASTICSEARCH:URL"]); + _logger.LogInformation("Indexing started"); + _logger.LogInformation("Using Elasticsearch at {ElasticsearchAddress:l}", _configuration["ELASTICSEARCH:URL"]); var configuredTypesAndIndexNames = _indexNameSettings.GetTypesAndIndexNames(); @@ -54,17 +54,20 @@ public async Task Start() if (repositoryForType is null) { - _logger.LogError("{EntityType}: Unable to find database repository for index {IndexName}", modelType.Name, indexName); + _logger.LogError("{EntityType:l}: Unable to find database repository for index {IndexName:l}", modelType.Name, indexName); continue; } + _logger.LogInformation("{EntityType:l}: Recreating index {IndexName:l}", modelType.Name, indexName); + await Task.Delay(1); // Force at least 1 ms separation to log timestamps to preserve log message order in OpenSearch. await IndexEntities(indexName, repositoryForType, modelType); + await Task.Delay(1); } - var totalTime = stopWatch.Elapsed; + var totalTime = stopWatchMain.Elapsed; - _logger.LogInformation("Indexing completed in {Elapsed}, finishing process", totalTime); - stopWatch.Stop(); + _logger.LogInformation("Indexing complete in {Elapsed}", totalTime); + stopWatchMain.Stop(); } /// @@ -73,7 +76,8 @@ public async Task Start() private async Task PopulateOrganizationCache() { _logger.LogInformation("Populating Organization cache"); - + Stopwatch stopWatchPopulateCache = new(); + stopWatchPopulateCache.Start(); var organizationRepository = _indexRepositories.SingleOrDefault(repo => repo.ModelType == typeof(Organization)); if (organizationRepository != null) { @@ -83,17 +87,18 @@ private async Task PopulateOrganizationCache() _memoryCache.Set(MemoryCacheKeys.OrganizationById(organization.Id), organization); } } - - _logger.LogInformation("Populated Organization cache"); + stopWatchPopulateCache.Stop(); + _logger.LogInformation("Populated Organization cache in {Elapsed}", stopWatchPopulateCache.Elapsed); } - + /// /// Gets all call programmes from the database to an in-memory cache to simplify SQL queries by automapper. /// private async Task PopulateFundingCallCache() { _logger.LogInformation("Populating Funding Call cache"); - + Stopwatch stopWatchPopulateCache = new(); + stopWatchPopulateCache.Start(); var fundingCallRepository = _indexRepositories.SingleOrDefault(repo => repo.ModelType == typeof(FundingCall)); if (fundingCallRepository != null) { @@ -106,36 +111,31 @@ private async Task PopulateFundingCallCache() { continue; } - + if (_memoryCache.TryGetValue(MemoryCacheKeys.FundingCallByAbbreviationAndEuCallId(fundingCall.Abbreviation, fundingCall.EuCallId), out List foundFundingCalls)) { foundFundingCalls.Add(fundingCall.SourceProgrammeId); } else { - _memoryCache.Set(MemoryCacheKeys.FundingCallByAbbreviationAndEuCallId(fundingCall.Abbreviation, fundingCall.EuCallId), new List { fundingCall.SourceProgrammeId }); + _memoryCache.Set(MemoryCacheKeys.FundingCallByAbbreviationAndEuCallId(fundingCall.Abbreviation, fundingCall.EuCallId), new List { fundingCall.SourceProgrammeId }); } } } - - _logger.LogInformation("Populated Funding Call cache"); + stopWatchPopulateCache.Stop(); + _logger.LogInformation("Populated Funding Call cache in {Elapsed}", stopWatchPopulateCache.Elapsed); } - private async Task IndexEntities(string indexName, - IIndexRepository repository, - Type type) + private async Task IndexEntities(string indexName, IIndexRepository repository, Type type) { - _logger.LogInformation("{EntityType}: Recreating '{IndexName}' index...", type.Name, indexName); - Stopwatch stopWatch = new(); - stopWatch.Start(); try { List finalized = new(); - if (indexName.Contains("publication")) { - + if (indexName.Contains("publication")) + { // Create new index var (indexToCreate, indexToDelete) = await _indexService.GetIndexNames(indexName); await _indexService.CreateIndex(indexToCreate, type); @@ -146,7 +146,7 @@ private async Task IndexEntities(string indexName, * Currently this is done only for publications, because their dataset is much * larger than others. */ - + int skipAmount = 0; int takeAmount = 50000; int numOfResults = 0; @@ -154,14 +154,18 @@ private async Task IndexEntities(string indexName, do { - _logger.LogInformation("{EntityType}: Requested {takeAmount} entities from database...", type.Name, takeAmount); + _logger.LogInformation("{EntityType:l}: Requesting {TakeAmount} entities from database", type.Name, takeAmount); + stopWatch.Start(); var indexModels = await repository.GetChunkAsync(skipAmount: skipAmount, takeAmount: takeAmount).ToListAsync(); + stopWatch.Stop(); numOfResults = indexModels.Count; - _logger.LogInformation("{EntityType}: ...received {numOfResults} entities", type.Name, numOfResults); - + _logger.LogInformation("{EntityType:l}: Received {DatabaseResultCount} entities in {Elapsed}", type.Name, numOfResults, stopWatch.Elapsed); + stopWatch.Reset(); + if (numOfResults > 0) { - foreach (object entity in indexModels) { + foreach (object entity in indexModels) + { finalized.Add(repository.PerformInMemoryOperation(entity)); } await _indexService.IndexChunkAsync(indexToCreate, finalized, type); @@ -169,11 +173,12 @@ private async Task IndexEntities(string indexName, skipAmount = skipAmount + takeAmount; processedCount = processedCount + numOfResults; finalized = new(); - _logger.LogInformation("{EntityType}: Total documents indexed = {processedCount}", type.Name, processedCount); - } while(numOfResults >= takeAmount-1); + _logger.LogInformation("{EntityType:l}: Total documents indexed = {processedCount}", type.Name, processedCount); + } while (numOfResults >= takeAmount - 1); // Activate new index and delete old await _indexService.SwitchIndexes(indexName, indexToCreate, indexToDelete); + _logger.LogInformation("{EntityType:l}: Recreated index {IndexName:l}, {ElasticsearchDocumentCount} documents", type.Name, indexName, processedCount); } else { @@ -181,36 +186,28 @@ private async Task IndexEntities(string indexName, * Process complete database result at once. * Suitable for small result sets. */ - _logger.LogInformation("{EntityType}: Requested all entities from database...", type.Name); + _logger.LogInformation("{EntityType:l}: Requested all entities from database", type.Name); + stopWatch.Start(); var indexModels = await repository.GetAllAsync().ToListAsync(); - var databaseElapsed = stopWatch.Elapsed; - _logger.LogInformation("{EntityType}: ..received {DatabaseCount} entities in {ElapsedDatabase}", type.Name, indexModels.Count, databaseElapsed); - + stopWatch.Stop(); + _logger.LogInformation("{EntityType:l}: Received {DatabaseResultCount} entities in {Elapsed}", type.Name, indexModels.Count, stopWatch.Elapsed); + if (indexModels.Count > 0) { - _logger.LogInformation("{EntityType}: Start in-memory operations", type.Name); finalized = repository.PerformInMemoryOperations(indexModels); } - var inMemoryElapsed = stopWatch.Elapsed; - if (finalized.Count > 0) { - _logger.LogInformation("{EntityType}: Retrieved and performed in-memory operations to {FinalizedCount} entities in {Elapsed}. Start indexing.", type.Name, finalized.Count, inMemoryElapsed); await _indexService.IndexAsync(indexName, finalized, type); var indexingElapsed = stopWatch.Elapsed; - _logger.LogInformation("{EntityType}: Indexed total of {IndexCount} documents in {ElapsedIndexing}...", type.Name, finalized.Count, indexingElapsed - inMemoryElapsed); - _logger.LogInformation("{EntityType}: Index '{IndexName}' recreated successfully in {ElapsedTotal}", type.Name, indexName, stopWatch.Elapsed); - } - else - { - _logger.LogInformation("{EntityType}: Nothing to index", type.Name); + _logger.LogInformation("{EntityType:l}: Recreated index {IndexName:l}, {ElasticsearchDocumentCount} documents", type.Name, indexName, finalized.Count); } } } catch (Exception ex) { - _logger.LogError(ex, "{EntityType}: Exception occurred while indexing '{IndexName}' index after {ElapsedException},", type.Name, indexName, stopWatch.Elapsed); + _logger.LogError("{EntityType:l}: Exception occurred while indexing {IndexName:l}: {IndexerException} index after {Elapsed},", type.Name, indexName, stopWatch.Elapsed, ex.ToString()); } } } \ No newline at end of file diff --git a/aspnetcore/src/Indexer/Program.cs b/aspnetcore/src/Indexer/Program.cs index 0f07714..71d883a 100644 --- a/aspnetcore/src/Indexer/Program.cs +++ b/aspnetcore/src/Indexer/Program.cs @@ -18,18 +18,19 @@ public class Program public static async Task Main(string[] args) { var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - var configuration = new ConfigurationBuilder() + var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{environment}.json", true) + .AddUserSecrets() .AddEnvironmentVariables() .Build(); - - Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .CreateLogger();; - // Create and configure the host to support dependency injection, configuration, etc. + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); + + // Create and configure the host to support dependency injection, configuration, etc. var consoleHost = CreateHostBuilder(args).Build(); // Get the "Main" service which handles the indexing. @@ -37,6 +38,9 @@ public static async Task Main(string[] args) // Start indexing. await indexer.Start(); + + // Flush logs + Log.CloseAndFlush(); } private static IHostBuilder CreateHostBuilder(string[] args) => Host @@ -58,11 +62,11 @@ private static IHostBuilder CreateHostBuilder(string[] args) => Host services.AddMemoryCache(); - if(!int.TryParse(hostContext.Configuration["QueryTimeout"], out var queryTimeout)) + if (!int.TryParse(hostContext.Configuration["QueryTimeout"], out var queryTimeout)) { queryTimeout = DefaultQueryTimeout; } - + // Configure db & entity framework. services.AddDbContext(options => { diff --git a/aspnetcore/src/Indexer/appsettings.Development.json b/aspnetcore/src/Indexer/appsettings.Development.json index a90f9fc..c98d0c6 100644 --- a/aspnetcore/src/Indexer/appsettings.Development.json +++ b/aspnetcore/src/Indexer/appsettings.Development.json @@ -27,6 +27,7 @@ "Args": { "requestUri": "http://localhost:9991", "httpClient": "CSC.PublicApi.Logging.BasicAuthenticationHttpClient, CSC.PublicApi.Logging, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "textFormatter": "Serilog.Sinks.Http.TextFormatters.WoodLogMetadataFormatterForIndexer, CSC.PublicApi.Logging", "queueLimitBytes": null } } diff --git a/aspnetcore/src/Indexer/appsettings.json b/aspnetcore/src/Indexer/appsettings.json index 56d3db7..9fa9bb6 100644 --- a/aspnetcore/src/Indexer/appsettings.json +++ b/aspnetcore/src/Indexer/appsettings.json @@ -23,7 +23,8 @@ "Name": "Http", "Args": { "queueLimitBytes": null, - "httpClient": "CSC.PublicApi.Logging.BasicAuthenticationHttpClient, CSC.PublicApi.Logging, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + "httpClient": "CSC.PublicApi.Logging.BasicAuthenticationHttpClient, CSC.PublicApi.Logging, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "textFormatter": "Serilog.Sinks.Http.TextFormatters.WoodLogMetadataFormatterForIndexer, CSC.PublicApi.Logging" } } }, diff --git a/aspnetcore/src/Interface/ApiConstants.cs b/aspnetcore/src/Interface/ApiConstants.cs index ea7dd7d..118c753 100644 --- a/aspnetcore/src/Interface/ApiConstants.cs +++ b/aspnetcore/src/Interface/ApiConstants.cs @@ -7,4 +7,11 @@ public static class ApiConstants { public const string ApiVersion1 = "1.0"; public const string ContentTypeJson = "application/json"; + public const string LogResourceType_PropertyName = "ResourceType"; + public const string LogResourceType_FundingCall = "funding_call"; + public const string LogResourceType_FundingDecision = "funding_decision"; + public const string LogResourceType_Infrastructure = "infrastucture"; + public const string LogResourceType_Organization = "organization"; + public const string LogResourceType_Publication = "publication"; + public const string LogResourceType_ResearchDataset = "research_dataset"; } \ No newline at end of file diff --git a/aspnetcore/src/Interface/Controllers/FundingCallController.cs b/aspnetcore/src/Interface/Controllers/FundingCallController.cs index e96ff83..c3da5b9 100644 --- a/aspnetcore/src/Interface/Controllers/FundingCallController.cs +++ b/aspnetcore/src/Interface/Controllers/FundingCallController.cs @@ -14,13 +14,17 @@ public class FundingCallController : ControllerBase { private readonly ILogger _logger; private readonly IFundingCallService _service; + private readonly IDiagnosticContext _diagnosticContext; public FundingCallController( ILogger logger, - IFundingCallService service) + IFundingCallService service, + IDiagnosticContext diagnosticContext) { _logger = logger; _service = service; + _diagnosticContext = diagnosticContext; + _diagnosticContext.Set(ApiConstants.LogResourceType_PropertyName, ApiConstants.LogResourceType_FundingCall); } /// diff --git a/aspnetcore/src/Interface/Controllers/FundingDecisionController.cs b/aspnetcore/src/Interface/Controllers/FundingDecisionController.cs index 40c5667..1f250a3 100644 --- a/aspnetcore/src/Interface/Controllers/FundingDecisionController.cs +++ b/aspnetcore/src/Interface/Controllers/FundingDecisionController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using ResearchFi.FundingDecision; using ResearchFi.Query; +using Serilog; namespace CSC.PublicApi.Interface.Controllers; @@ -15,13 +16,17 @@ public class FundingDecisionController : ControllerBase private const string ApiVersion = "1.0"; private readonly ILogger _logger; private readonly IFundingDecisionService _service; + private readonly IDiagnosticContext _diagnosticContext; public FundingDecisionController( ILogger logger, - IFundingDecisionService service) + IFundingDecisionService service, + IDiagnosticContext diagnosticContext) { _logger = logger; _service = service; + _diagnosticContext = diagnosticContext; + _diagnosticContext.Set(ApiConstants.LogResourceType_PropertyName, ApiConstants.LogResourceType_FundingDecision); } /// diff --git a/aspnetcore/src/Interface/Controllers/InfrastructureController.cs b/aspnetcore/src/Interface/Controllers/InfrastructureController.cs index ae1fa5a..44e1932 100644 --- a/aspnetcore/src/Interface/Controllers/InfrastructureController.cs +++ b/aspnetcore/src/Interface/Controllers/InfrastructureController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using ResearchFi.Query; using Infrastructure =ResearchFi.Infrastructure.Infrastructure; +using Serilog; namespace CSC.PublicApi.Interface.Controllers; @@ -15,13 +16,17 @@ public class InfrastructureController : ControllerBase private readonly ILogger _logger; private IInfrastructureService _service; + private readonly IDiagnosticContext _diagnosticContext; public InfrastructureController( ILogger logger, - IInfrastructureService service) + IInfrastructureService service, + IDiagnosticContext diagnosticContext) { _logger = logger; _service = service; + _diagnosticContext = diagnosticContext; + _diagnosticContext.Set(ApiConstants.LogResourceType_PropertyName, ApiConstants.LogResourceType_Infrastructure); } /// diff --git a/aspnetcore/src/Interface/Controllers/OrganizationController.cs b/aspnetcore/src/Interface/Controllers/OrganizationController.cs index a6bbf9e..983cf14 100644 --- a/aspnetcore/src/Interface/Controllers/OrganizationController.cs +++ b/aspnetcore/src/Interface/Controllers/OrganizationController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using ResearchFi.Query; using Organization = ResearchFi.Organization.Organization; +using Serilog; namespace CSC.PublicApi.Interface.Controllers; @@ -15,13 +16,17 @@ public class OrganizationController : ControllerBase private readonly ILogger _logger; private IOrganizationService _service; + private readonly IDiagnosticContext _diagnosticContext; public OrganizationController( ILogger logger, - IOrganizationService service) + IOrganizationService service, + IDiagnosticContext diagnosticContext) { _logger = logger; _service = service; + _diagnosticContext = diagnosticContext; + _diagnosticContext.Set(ApiConstants.LogResourceType_PropertyName, ApiConstants.LogResourceType_Organization); } /// diff --git a/aspnetcore/src/Interface/Controllers/PublicationController.cs b/aspnetcore/src/Interface/Controllers/PublicationController.cs index c16e1ca..baebcfe 100644 --- a/aspnetcore/src/Interface/Controllers/PublicationController.cs +++ b/aspnetcore/src/Interface/Controllers/PublicationController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using ResearchFi.Query; +using Serilog; namespace CSC.PublicApi.Interface.Controllers; @@ -14,13 +15,17 @@ public class PublicationController : ControllerBase { private readonly ILogger _logger; private IPublicationService _service; + private readonly IDiagnosticContext _diagnosticContext; public PublicationController( ILogger logger, - IPublicationService service) + IPublicationService service, + IDiagnosticContext diagnosticContext) { _logger = logger; _service = service; + _diagnosticContext = diagnosticContext; + _diagnosticContext.Set(ApiConstants.LogResourceType_PropertyName, ApiConstants.LogResourceType_Publication); } /// @@ -36,7 +41,7 @@ public PublicationController( [Consumes(ApiConstants.ContentTypeJson)] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] - [ProducesResponseType(typeof(void),StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)] public async Task> Get([FromQuery] GetPublicationsQueryParameters queryParameters) { var (publications, searchResult) = await _service.GetPublications(queryParameters); @@ -55,13 +60,13 @@ public async Task> Get([FromQuery] GetPublicationsQuery /// Unauthorized. /// Forbidden. /// Not found. - [HttpGet("{publicationId}",Name = "GetPublication")] + [HttpGet("{publicationId}", Name = "GetPublication")] [Authorize(Policy = ApiPolicies.Publication.Read)] [Produces(ApiConstants.ContentTypeJson)] [ProducesResponseType(typeof(Publication), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(void),StatusCodes.Status401Unauthorized)] - [ProducesResponseType(typeof(void),StatusCodes.Status403Forbidden)] - [ProducesResponseType(typeof(void),StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] public async Task Get(string publicationId) { var publication = await _service.GetPublication(publicationId); diff --git a/aspnetcore/src/Interface/Controllers/ResearchDatasetController.cs b/aspnetcore/src/Interface/Controllers/ResearchDatasetController.cs index a92abd8..02661b2 100644 --- a/aspnetcore/src/Interface/Controllers/ResearchDatasetController.cs +++ b/aspnetcore/src/Interface/Controllers/ResearchDatasetController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using ResearchFi.Query; using ResearchDataset = ResearchFi.ResearchDataset.ResearchDataset; +using Serilog; namespace CSC.PublicApi.Interface.Controllers; @@ -15,13 +16,17 @@ public class ResearchDatasetController : ControllerBase private readonly ILogger _logger; private IResearchDatasetService _service; + private readonly IDiagnosticContext _diagnosticContext; public ResearchDatasetController( ILogger logger, - IResearchDatasetService service) + IResearchDatasetService service, + IDiagnosticContext diagnosticContext) { _logger = logger; _service = service; + _diagnosticContext = diagnosticContext; + _diagnosticContext.Set(ApiConstants.LogResourceType_PropertyName, ApiConstants.LogResourceType_ResearchDataset); } /// diff --git a/aspnetcore/src/Interface/Middleware/CorrelationIdMiddleware.cs b/aspnetcore/src/Interface/Middleware/CorrelationIdMiddleware.cs index 853e654..3138c31 100644 --- a/aspnetcore/src/Interface/Middleware/CorrelationIdMiddleware.cs +++ b/aspnetcore/src/Interface/Middleware/CorrelationIdMiddleware.cs @@ -41,7 +41,7 @@ public async Task InvokeAsync(HttpContext context) var clientId = context.User?.Claims.FirstOrDefault(claim => claim.Type == "clientId")?.Value; var organizationId = context.User?.Claims.FirstOrDefault(claim => claim.Type == "organizationid")?.Value; - _logger.LogDebug("Correlation id '{correlationId}' generated for '{clientId}' '{organizationId}'.", correlationId, clientId, organizationId); + _logger.LogDebug("Correlation Id '{CorrelationId:l}' generated for '{ClientId:l}' '{OrganizationId:l}'.", correlationId, clientId, organizationId); await _next(context); } diff --git a/aspnetcore/src/Interface/Middleware/GlobalErrorHandlerMiddleware.cs b/aspnetcore/src/Interface/Middleware/GlobalErrorHandlerMiddleware.cs index 5fff33b..f255f42 100644 --- a/aspnetcore/src/Interface/Middleware/GlobalErrorHandlerMiddleware.cs +++ b/aspnetcore/src/Interface/Middleware/GlobalErrorHandlerMiddleware.cs @@ -30,11 +30,11 @@ public async Task InvokeAsync(HttpContext context) ? id : "N/A"; - _logger.LogError(exception, "Global error handler caught an exception. CorrelationId: '{correlationID}'.", correlationId); + _logger.LogError(exception, "Global error handler caught an exception. CorrelationId: '{CorrelationID}'.", correlationId); var error = JsonSerializer.Serialize(new { - message = $"Error. Correlation id: {correlationId}" + message = $"Error. Correlation Id: {correlationId}" }); await response.WriteAsync(error); diff --git a/aspnetcore/src/Interface/Program.cs b/aspnetcore/src/Interface/Program.cs index 3caea9f..00ae6bd 100644 --- a/aspnetcore/src/Interface/Program.cs +++ b/aspnetcore/src/Interface/Program.cs @@ -76,15 +76,17 @@ app.UseMiddleware(); // Add properties from the HTTP request to the logging. -app.UseSerilogRequestLogging(opts => opts.EnrichDiagnosticContext = (context, httpContext) => +app.UseSerilogRequestLogging(opts => opts.EnrichDiagnosticContext = (IDiagnosticContext context, HttpContext httpContext) => { var correlationId = httpContext.Items[CorrelationIdMiddleware.CorrelationIdHeaderName]; var clientId = httpContext.User?.Claims.FirstOrDefault(claim => claim.Type == "clientId")?.Value; var organizationId = httpContext.User?.Claims.FirstOrDefault(claim => claim.Type == "organizationid")?.Value; + var queryString = httpContext.Request.QueryString.HasValue ? httpContext.Request.QueryString.Value : ""; - context.Set("correlationId", correlationId); - context.Set("clientId", clientId); - context.Set("organizationId", organizationId); + context.Set("CorrelationId", correlationId); + context.Set("ClientId", clientId); + context.Set("OrganizationId", organizationId); + context.Set("QueryString", queryString); }); app.UseAuthentication(); diff --git a/aspnetcore/src/Interface/appsettings.Development.json b/aspnetcore/src/Interface/appsettings.Development.json index 9b2b895..2af47f9 100644 --- a/aspnetcore/src/Interface/appsettings.Development.json +++ b/aspnetcore/src/Interface/appsettings.Development.json @@ -20,7 +20,7 @@ "ConsoleSink": { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}][{clientId}, {ClientIp}, {ClientAgent}]: {Message}{NewLine}{Exception}" + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}][{ClientId}, {ClientIp}, {ClientAgent}, {QueryString}]: {Message}{NewLine}{Exception}" } }, "HttpSink": { @@ -28,6 +28,7 @@ "Args": { "requestUri": "http://localhost:9991", "httpClient": "CSC.PublicApi.Logging.BasicAuthenticationHttpClient, CSC.PublicApi.Logging, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "textFormatter": "Serilog.Sinks.Http.TextFormatters.WoodLogMetadataFormatterForInterface, CSC.PublicApi.Logging", "queueLimitBytes": null } } diff --git a/aspnetcore/src/Interface/appsettings.json b/aspnetcore/src/Interface/appsettings.json index db4ccc9..6b17888 100644 --- a/aspnetcore/src/Interface/appsettings.json +++ b/aspnetcore/src/Interface/appsettings.json @@ -16,14 +16,15 @@ "ConsoleSink": { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}][{clientId}, {ClientIp}, {ClientAgent}]: {Message}{NewLine}{Exception}" + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}][{ClientId}, {ClientIp}, {ClientAgent}, {QueryString}]: {Message}{NewLine}{Exception}" } }, "HttpSink": { "Name": "Http", "Args": { "queueLimitBytes": null, - "httpClient": "CSC.PublicApi.Logging.BasicAuthenticationHttpClient, CSC.PublicApi.Logging, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" + "httpClient": "CSC.PublicApi.Logging.BasicAuthenticationHttpClient, CSC.PublicApi.Logging, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "textFormatter": "Serilog.Sinks.Http.TextFormatters.WoodLogMetadataFormatterForInterface, CSC.PublicApi.Logging" } } }, diff --git a/aspnetcore/src/Logging/HttpSinkWoodLogMetadataFormatterBase.cs b/aspnetcore/src/Logging/HttpSinkWoodLogMetadataFormatterBase.cs new file mode 100644 index 0000000..2edd3da --- /dev/null +++ b/aspnetcore/src/Logging/HttpSinkWoodLogMetadataFormatterBase.cs @@ -0,0 +1,278 @@ +// Copyright 2015-2023 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Serilog.Debugging; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Formatting.Json; +using Serilog.Parsing; + +namespace Serilog.Sinks.Http.TextFormatters; + +/// +/// JSON formatter serializing log events into a normal format with its data normalized. The +/// lack of a rendered message means improved network load compared to +/// . Often this formatter is complemented with a log +/// server that is capable of rendering the messages of the incoming log events. +/// +/// +/// +/// +/// +/// +public class WoodLogMetadataFormatterBase : ITextFormatter +{ + /// + /// Gets or sets a value indicating whether the message template is included into JSON. + /// + protected bool IncludeMessageTemplate { get; set; } + + /// + /// Gets or sets a value indicating whether the message is rendered into JSON. + /// + protected bool IncludeRenderedMessage { get; set; } + + /// + /// Gets or sets a value indicating which application is being logged (Indexer or Interface). + /// + protected string ApplicationType { get; set; } + + + /// + /// Format the log event into the output. + /// + /// The event to format. + /// The output. + public void Format(LogEvent logEvent, TextWriter output) + { + try + { + var buffer = new StringWriter(); + FormatContent(logEvent, buffer); + + // If formatting was successful, write to output + output.WriteLine(buffer.ToString()); + } + catch (Exception e) + { + LogNonFormattableEvent(logEvent, e); + } + } + + private void FormatContent(LogEvent logEvent, TextWriter output) + { + if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + if (output == null) throw new ArgumentNullException(nameof(output)); + + output.Write("{\"Timestamp\":\""); + output.Write(logEvent.Timestamp.UtcDateTime.ToString("o")); + + output.Write("\",\"Level\":\""); + output.Write(logEvent.Level); + output.Write("\""); + + if (IncludeMessageTemplate) + { + output.Write(",\"MessageTemplate\":"); + JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output); + } + + if (IncludeRenderedMessage) + { + output.Write(",\"RenderedMessage\":"); + + var message = logEvent.MessageTemplate.Render(logEvent.Properties); + JsonValueFormatter.WriteQuotedJsonString(message, output); + } + + if (logEvent.Exception != null) + { + output.Write(",\"Exception\":"); + JsonValueFormatter.WriteQuotedJsonString(logEvent.Exception.ToString(), output); + } + + if (logEvent.Properties.Count != 0) + { + WriteProperties(logEvent.Properties, output, ApplicationType); + } + + // Better not to allocate an array in the 99.9% of cases where this is false + var tokensWithFormat = logEvent.MessageTemplate.Tokens + .OfType() + .Where(pt => pt.Format != null); + + // ReSharper disable once PossibleMultipleEnumeration + if (tokensWithFormat.Any()) + { + // ReSharper disable once PossibleMultipleEnumeration + WriteRenderings(tokensWithFormat.GroupBy(pt => pt.PropertyName), logEvent.Properties, output); + } + + output.Write('}'); + } + + private static void WriteProperties( + IReadOnlyDictionary properties, + TextWriter output, + string ApplicationType) + { + output.Write(",\"Properties\":{"); + + var precedingDelimiter = string.Empty; + + /* + * Handle Wood log metadata specific properties and construct + * metadata for Wood log server. + * + * Values are grabbed from Serilog configuration. + * + * Wood project number: + * Serilog.Properties.WoodLogProjectNumber + * Wood use case: + * Serilog.Properties.WoodLogUseCaseIndexer + * Serilog.Properties.WoodLogUseCaseInterface + * Wood retention months: + * Serilog.Properties.WoodLogRetentionMonthsIndexer + * Serilog.Properties.WoodLogRetentionMonthsInterface + * + * Properties are excluded from the "Properties" section of + * outgoing JSON log message. Instead, a new top level + * key "wood" is added and it's value contains Wood metadata. + * + * Example output: + * + * { + * "Timestamp": "2023-09-01T07:49:00.0700050Z", + * "Level": "Information", + * "MessageTemplate": "Application starting up", + * "Properties": { + * "MachineName": "devel123", + * "Application": "CSC.PublicApi" + * }, + * "wood": { + * "project_number": "12341234", + * "use_case": "publicapi_indexer_devel", + * "retention_months": "1" + * } + * } + */ + KeyValuePair woodProjectNumber = new(); + KeyValuePair woodUseCase = new(); + KeyValuePair woodRetentionMonths = new(); + + foreach (var property in properties) + { + if (property.Key.ToLower() == "woodlogprojectnumber") + { + woodProjectNumber = property; + } + else if (ApplicationType == "Indexer" && property.Key.ToLower() == "woodlogusecaseindexer") + { + woodUseCase = property; + } + else if (ApplicationType == "Interface" && property.Key.ToLower() == "woodlogusecaseinterface") + { + woodUseCase = property; + } + else if (ApplicationType == "Indexer" && property.Key.ToLower() == "woodlogretentionmonthsindexer") + { + woodRetentionMonths = property; + } + else if (ApplicationType == "Interface" && property.Key.ToLower() == "woodlogretentionmonthsinterface") + { + woodRetentionMonths = property; + } + else + { + output.Write(precedingDelimiter); + precedingDelimiter = ","; + + JsonValueFormatter.WriteQuotedJsonString(property.Key, output); + output.Write(':'); + ValueFormatter.Instance.Format(property.Value, output); + } + } + + output.Write('}'); + + // Write Wood log metadata + output.Write(",\"wood\":{"); + // Project number + JsonValueFormatter.WriteQuotedJsonString("project_number", output); + output.Write(':'); + ValueFormatter.Instance.Format(woodProjectNumber.Value, output); + output.Write(','); + // Use case + JsonValueFormatter.WriteQuotedJsonString("use_case", output); + output.Write(':'); + ValueFormatter.Instance.Format(woodUseCase.Value, output); + output.Write(','); + // Retention months + JsonValueFormatter.WriteQuotedJsonString("retention_months", output); + output.Write(':'); + ValueFormatter.Instance.Format(woodRetentionMonths.Value, output); + output.Write('}'); + } + + private static void WriteRenderings( + IEnumerable> tokensWithFormat, + IReadOnlyDictionary properties, + TextWriter output) + { + output.Write(",\"Renderings\":{"); + + var rdelim = string.Empty; + foreach (var ptoken in tokensWithFormat) + { + output.Write(rdelim); + rdelim = ","; + + JsonValueFormatter.WriteQuotedJsonString(ptoken.Key, output); + output.Write(":["); + + var fdelim = string.Empty; + foreach (var format in ptoken) + { + output.Write(fdelim); + fdelim = ","; + + output.Write("{\"Format\":"); + JsonValueFormatter.WriteQuotedJsonString(format.Format, output); + + output.Write(",\"Rendering\":"); + var sw = new StringWriter(); + format.Render(properties, sw); + JsonValueFormatter.WriteQuotedJsonString(sw.ToString(), output); + output.Write('}'); + } + + output.Write(']'); + } + + output.Write('}'); + } + + private static void LogNonFormattableEvent(LogEvent logEvent, Exception e) + { + SelfLog.WriteLine( + "Event at {0} with message template {1} could not be formatted into JSON and will be dropped: {2}", + logEvent.Timestamp.ToString("o"), + logEvent.MessageTemplate.Text, + e); + } +} \ No newline at end of file diff --git a/aspnetcore/src/Logging/HttpSinkWoodLogMetadataFormatterForIndexer.cs b/aspnetcore/src/Logging/HttpSinkWoodLogMetadataFormatterForIndexer.cs new file mode 100644 index 0000000..bdf04ba --- /dev/null +++ b/aspnetcore/src/Logging/HttpSinkWoodLogMetadataFormatterForIndexer.cs @@ -0,0 +1,11 @@ +namespace Serilog.Sinks.Http.TextFormatters; +public class WoodLogMetadataFormatterForIndexer : WoodLogMetadataFormatterBase +{ + // create constructor + public WoodLogMetadataFormatterForIndexer() + { + IncludeMessageTemplate = false; + IncludeRenderedMessage = true; + ApplicationType = "Indexer"; + } +} \ No newline at end of file diff --git a/aspnetcore/src/Logging/HttpSinkWoodLogMetadataFormatterForInterface.cs b/aspnetcore/src/Logging/HttpSinkWoodLogMetadataFormatterForInterface.cs new file mode 100644 index 0000000..142e5d7 --- /dev/null +++ b/aspnetcore/src/Logging/HttpSinkWoodLogMetadataFormatterForInterface.cs @@ -0,0 +1,11 @@ +namespace Serilog.Sinks.Http.TextFormatters; +public class WoodLogMetadataFormatterForInterface : WoodLogMetadataFormatterBase +{ + // create constructor + public WoodLogMetadataFormatterForInterface() + { + IncludeMessageTemplate = false; + IncludeRenderedMessage = false; + ApplicationType = "Interface"; + } +} \ No newline at end of file diff --git a/aspnetcore/src/Logging/ValueFormatter.cs b/aspnetcore/src/Logging/ValueFormatter.cs new file mode 100644 index 0000000..43924be --- /dev/null +++ b/aspnetcore/src/Logging/ValueFormatter.cs @@ -0,0 +1,22 @@ +// Copyright 2015-2023 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Serilog.Formatting.Json; + +namespace Serilog.Sinks.Http.TextFormatters; + +internal static class ValueFormatter +{ + internal static readonly JsonValueFormatter Instance = new(); +} \ No newline at end of file diff --git a/aspnetcore/test/Interface.Tests/FundingCallControllerTest.cs b/aspnetcore/test/Interface.Tests/FundingCallControllerTest.cs index 871a161..8392df7 100644 --- a/aspnetcore/test/Interface.Tests/FundingCallControllerTest.cs +++ b/aspnetcore/test/Interface.Tests/FundingCallControllerTest.cs @@ -11,6 +11,7 @@ using Moq; using ResearchFi.Query; using Xunit; +using Serilog; namespace CSC.PublicApi.Interface.Tests; @@ -19,6 +20,7 @@ public class FundingCallControllerTest : IDisposable private readonly FundingCallController _controller; private readonly Mock> _mockSearchService; private readonly IFundingCallService _service; + private readonly IDiagnosticContext _diagnosticContext; public FundingCallControllerTest() { @@ -29,8 +31,9 @@ public FundingCallControllerTest() _mockSearchService = new Mock>(); var mapper = new MapperConfiguration(cfg => cfg.AddProfile()).CreateMapper(); _service = new FundingCallService(serviceLogger, mapper, _mockSearchService.Object); + _diagnosticContext = new Mock().Object;; - _controller = new FundingCallController(controllerLogger, _service); + _controller = new FundingCallController(controllerLogger, _service, _diagnosticContext); } [Fact]