feat(news): dynamic settings, IFinlyticLogger, live log streaming, and EF migration

This commit is contained in:
2026-08-15 21:30:01 +02:00
parent a1f2b888f6
commit a94c36a878
12 changed files with 800 additions and 555 deletions
+16 -20
View File
@@ -1,14 +1,18 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using FinlyticCore.Dtos.News;
using FinlyticCore.Services;
using FinlyticCore.Util;
using FinlyticNews.Util;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace FinlyticNews.Services;
using FinlyticCore.Dtos.News;
/// <summary>
/// Defines integration operations with the external n8n AI workflow webhook.
/// </summary>
@@ -26,21 +30,18 @@ public class N8nService : IN8nService
private readonly HttpClient _httpClient;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IConfiguration _configuration;
private readonly ILogger<N8nService> _logger;
private readonly IFinlyticLogger<N8nService> _finlyticLogger;
/// <summary>
/// Initializes a new instance of the <see cref="N8nService"/> class.
/// </summary>
public N8nService(
HttpClient httpClient,
IServiceScopeFactory scopeFactory,
IConfiguration configuration,
ILogger<N8nService> logger)
IFinlyticLogger<N8nService> finlyticLogger)
{
_httpClient = httpClient;
_scopeFactory = scopeFactory;
_configuration = configuration;
_logger = logger;
_finlyticLogger = finlyticLogger;
}
/// <inheritdoc />
@@ -48,7 +49,6 @@ public class N8nService : IN8nService
{
string? targetUrl = null;
// 1. Dynamic Settings Resolution (DB Scope -> AppSettings Fallback)
using (var scope = _scopeFactory.CreateScope())
{
var settingsDb = scope.ServiceProvider.GetService<ISettingsDbService>();
@@ -67,17 +67,16 @@ public class N8nService : IN8nService
if (string.IsNullOrWhiteSpace(targetUrl))
{
_logger.LogError("[{Channel}] N8nWebhookUrl is not configured in DB or application settings.", "NewsChannel");
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, "[N8nService] N8nWebhookUrl is not configured in DB or application settings.");
return null;
}
_logger.LogInformation("[{Channel}] Posting article to n8n webhook pipeline at: {Url}", "NewsChannel", targetUrl);
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[N8nService] Posting article to n8n webhook pipeline at: {Url}", targetUrl);
var payload = new N8nRequestPayload(content, filteredAssets ?? []);
try
{
// Zero-Allocation / Source-Generated Request Serialization
var jsonContent = JsonSerializer.Serialize(payload, FinlyticJsonSerializerContext.Default.N8nRequestPayload);
using var requestContent = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
@@ -86,7 +85,7 @@ public class N8nService : IN8nService
if (!response.IsSuccessStatusCode)
{
var errorMsg = await response.Content.ReadAsStringAsync(ct);
_logger.LogError("[{Channel}] n8n webhook returned status code {StatusCode}. Error payload: {Error}", "NewsChannel", response.StatusCode, errorMsg);
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, "[N8nService] n8n webhook returned status code {StatusCode}. Error payload: {Error}", response.StatusCode, errorMsg);
return null;
}
@@ -95,18 +94,16 @@ public class N8nService : IN8nService
var root = doc.RootElement;
// 2. Robust n8n Array-Unwrapping (Handles [{ "json": { ... } }])
if (root.ValueKind == JsonValueKind.Array)
{
if (root.GetArrayLength() == 0)
{
_logger.LogWarning("[{Channel}] n8n webhook returned an empty array.", "NewsChannel");
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[N8nService] n8n webhook returned an empty array.");
return null;
}
root = root[0];
}
// 3. Dynamic Node Wrapper Unwrapping ("json", "output", "data", "body")
if (root.ValueKind == JsonValueKind.Object)
{
if (root.TryGetProperty("json", out var jsonChild) && jsonChild.ValueKind == JsonValueKind.Object)
@@ -119,13 +116,12 @@ public class N8nService : IN8nService
root = bodyChild;
}
// 4. Source-Generated Deserialization directly from JsonElement
var result = root.Deserialize(FinlyticJsonSerializerContext.Default.N8nResponsePayload);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Failed to communicate with or parse response from n8n webhook workflow.", "NewsChannel");
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[N8nService] Failed to communicate with or parse response from n8n webhook workflow.");
return null;
}
}