chore(infra): update compose configurations, mqtt health bridge, and export scripts
This commit is contained in:
@@ -23,6 +23,7 @@ $images = @(
|
|||||||
"finlyticengine",
|
"finlyticengine",
|
||||||
"finlyticsimulation",
|
"finlyticsimulation",
|
||||||
"finlyticbot",
|
"finlyticbot",
|
||||||
|
"finlyticnotify",
|
||||||
"finlyticbackend"
|
"finlyticbackend"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ public class AdminSettingsController : ControllerBase
|
|||||||
["FinlyticAnalyzer"] = "engine",
|
["FinlyticAnalyzer"] = "engine",
|
||||||
["FinlyticTrades"] = "engine",
|
["FinlyticTrades"] = "engine",
|
||||||
["FinlyticBot"] = "bot",
|
["FinlyticBot"] = "bot",
|
||||||
|
["FinlyticNotify"] = "notify",
|
||||||
// FinlyticSimulation was previously missing from this map entirely, so its settings
|
// FinlyticSimulation was previously missing from this map entirely, so its settings
|
||||||
// (SimulationSettingKeys: slippage/fee defaults, matrix-recompute schedule, etc.) never showed up
|
// (SimulationSettingKeys: slippage/fee defaults, matrix-recompute schedule, etc.) never showed up
|
||||||
// in the admin UI's settings screen even though the sim_settings_GetAll/Update RPC channels exist.
|
// in the admin UI's settings screen even though the sim_settings_GetAll/Update RPC channels exist.
|
||||||
@@ -209,6 +210,7 @@ public class AdminSettingsController : ControllerBase
|
|||||||
("FinlyticFundamentals", $"{MqttTopics.Channels.HealthPing}/FinlyticFundamentals", "Financial Statements & Estimates", "PostgreSQL fundamentals"),
|
("FinlyticFundamentals", $"{MqttTopics.Channels.HealthPing}/FinlyticFundamentals", "Financial Statements & Estimates", "PostgreSQL fundamentals"),
|
||||||
("FinlyticEngine", $"{MqttTopics.Channels.HealthPing}/FinlyticEngine", "Strategy Screener & Signals", "PostgreSQL engine"),
|
("FinlyticEngine", $"{MqttTopics.Channels.HealthPing}/FinlyticEngine", "Strategy Screener & Signals", "PostgreSQL engine"),
|
||||||
("FinlyticBot", $"{MqttTopics.Channels.HealthPing}/FinlyticBot", "Automated Trading Execution", "PostgreSQL bot"),
|
("FinlyticBot", $"{MqttTopics.Channels.HealthPing}/FinlyticBot", "Automated Trading Execution", "PostgreSQL bot"),
|
||||||
|
("FinlyticNotify", $"{MqttTopics.Channels.HealthPing}/FinlyticNotify", "ntfy Push Notifications", "PostgreSQL / Mosquitto"),
|
||||||
};
|
};
|
||||||
|
|
||||||
var results = new List<ServiceHealthStatusDto>
|
var results = new List<ServiceHealthStatusDto>
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ app.UseExceptionHandler();
|
|||||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
{
|
{
|
||||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost,
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost,
|
||||||
KnownNetworks = { },
|
KnownIPNetworks = { },
|
||||||
KnownProxies = { }
|
KnownProxies = { }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ public class SystemHealthBackgroundService : BackgroundService
|
|||||||
("FinlyticFundamentals", "health_Ping/FinlyticFundamentals", "Financial Statements & Estimates", "PostgreSQL fundamentals"),
|
("FinlyticFundamentals", "health_Ping/FinlyticFundamentals", "Financial Statements & Estimates", "PostgreSQL fundamentals"),
|
||||||
("FinlyticEngine", "health_Ping/FinlyticEngine", "Strategy Screener & Signals", "PostgreSQL engine"),
|
("FinlyticEngine", "health_Ping/FinlyticEngine", "Strategy Screener & Signals", "PostgreSQL engine"),
|
||||||
("FinlyticBot", "health_Ping/FinlyticBot", "Automated Trading Execution", "PostgreSQL bot"),
|
("FinlyticBot", "health_Ping/FinlyticBot", "Automated Trading Execution", "PostgreSQL bot"),
|
||||||
|
("FinlyticNotify", "health_Ping/FinlyticNotify", "ntfy Push Notifications", "PostgreSQL / Mosquitto"),
|
||||||
};
|
};
|
||||||
|
|
||||||
var results = new List<ServiceHealthStatusDto>
|
var results = new List<ServiceHealthStatusDto>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using FinlyticBackend.Database;
|
|||||||
using FinlyticBackend.Hubs;
|
using FinlyticBackend.Hubs;
|
||||||
using FinlyticBackend.Services;
|
using FinlyticBackend.Services;
|
||||||
using FinlyticBackend.Settings;
|
using FinlyticBackend.Settings;
|
||||||
|
using FinlyticCore.Dtos;
|
||||||
using FinlyticCore.Dtos.Bot;
|
using FinlyticCore.Dtos.Bot;
|
||||||
using FinlyticCore.Dtos.Logging;
|
using FinlyticCore.Dtos.Logging;
|
||||||
using FinlyticCore.Dtos.News;
|
using FinlyticCore.Dtos.News;
|
||||||
@@ -104,6 +105,11 @@ public class BackendMqttBridge : ManagedMqttClient, IHostedService
|
|||||||
MqttTopics.RequestFilter(MqttTopics.Channels.BackendGetAggregatedFavorites),
|
MqttTopics.RequestFilter(MqttTopics.Channels.BackendGetAggregatedFavorites),
|
||||||
HandleGetAggregatedFavoritesRpcAsync);
|
HandleGetAggregatedFavoritesRpcAsync);
|
||||||
|
|
||||||
|
// Username lookup RPC Endpoint for FinlyticNotify
|
||||||
|
await SubscribeRpcAsync<UserIdRequest, string?>(
|
||||||
|
MqttTopics.RequestFilter(MqttTopics.Channels.BackendGetUsername),
|
||||||
|
HandleGetUsernameRpcAsync);
|
||||||
|
|
||||||
// FinlyticBackend previously never broadcast its OWN structured logs at all (it only relayed other
|
// FinlyticBackend previously never broadcast its OWN structured logs at all (it only relayed other
|
||||||
// services' logs received on MqttTopics.LogsWildcard, subscribed above) - it never used
|
// services' logs received on MqttTopics.LogsWildcard, subscribed above) - it never used
|
||||||
// IFinlyticLogger<T>, so FinlyticLogBroadcaster.Broadcast was never invoked for anything happening
|
// IFinlyticLogger<T>, so FinlyticLogBroadcaster.Broadcast was never invoked for anything happening
|
||||||
@@ -286,4 +292,24 @@ public class BackendMqttBridge : ManagedMqttClient, IHostedService
|
|||||||
"[BackendMqttBridge] Responded to backend_GetAggregatedFavorites with {Count} unique ISINs. [CorrelationId: {CorrelationId}]", isins.Count, correlationId);
|
"[BackendMqttBridge] Responded to backend_GetAggregatedFavorites with {Count} unique ISINs. [CorrelationId: {CorrelationId}]", isins.Count, correlationId);
|
||||||
return isins;
|
return isins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string?> HandleGetUsernameRpcAsync(UserIdRequest? req, string correlationId)
|
||||||
|
{
|
||||||
|
if (req == null || req.UserId == Guid.Empty) return null;
|
||||||
|
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<BackendDbContext>();
|
||||||
|
|
||||||
|
var user = await dbContext.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == req.UserId);
|
||||||
|
|
||||||
|
if (user == null) return null;
|
||||||
|
|
||||||
|
string username = !string.IsNullOrWhiteSpace(user.FullName)
|
||||||
|
? user.FullName.Trim().ToLowerInvariant().Replace(" ", "_")
|
||||||
|
: user.Email.Split('@')[0].Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
return username;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -245,6 +245,13 @@ public record CreateManualTradeRequest(
|
|||||||
[property: JsonPropertyName("fee")] decimal Fee = 0m
|
[property: JsonPropertyName("fee")] decimal Fee = 0m
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generic request payload carrying a user GUID to resolve user identity across microservices.
|
||||||
|
/// </summary>
|
||||||
|
public record UserIdRequest(
|
||||||
|
[property: JsonPropertyName("userId")] Guid UserId
|
||||||
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Machine-readable classification of a server-side RPC fault, carried by <see cref="RpcErrorResponse"/> so a
|
/// Machine-readable classification of a server-side RPC fault, carried by <see cref="RpcErrorResponse"/> so a
|
||||||
/// caller can react to the specific failure mode instead of only learning "something went wrong" (or, before
|
/// caller can react to the specific failure mode instead of only learning "something went wrong" (or, before
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ public record TradeFillDto(
|
|||||||
public record ActiveTradeDto(
|
public record ActiveTradeDto(
|
||||||
[property: JsonPropertyName("tradeId")] Guid TradeId,
|
[property: JsonPropertyName("tradeId")] Guid TradeId,
|
||||||
[property: JsonPropertyName("proposalId")] Guid ProposalId,
|
[property: JsonPropertyName("proposalId")] Guid ProposalId,
|
||||||
|
[property: JsonPropertyName("userId")] Guid UserId,
|
||||||
[property: JsonPropertyName("underlyingIsin")] string UnderlyingIsin,
|
[property: JsonPropertyName("underlyingIsin")] string UnderlyingIsin,
|
||||||
[property: JsonPropertyName("symbol")] string Symbol,
|
[property: JsonPropertyName("symbol")] string Symbol,
|
||||||
[property: JsonPropertyName("derivativeIsin")] string? DerivativeIsin,
|
[property: JsonPropertyName("derivativeIsin")] string? DerivativeIsin,
|
||||||
|
|||||||
@@ -279,6 +279,14 @@ public static class MqttTopics
|
|||||||
/// <summary>Served by FinlyticBot: applies dynamic setting updates for the service.</summary>
|
/// <summary>Served by FinlyticBot: applies dynamic setting updates for the service.</summary>
|
||||||
public const string BotSettingsUpdate = "bot_settings_Update";
|
public const string BotSettingsUpdate = "bot_settings_Update";
|
||||||
|
|
||||||
|
// ---- FinlyticNotify ----
|
||||||
|
|
||||||
|
/// <summary>Served by FinlyticNotify: returns all dynamic settings for the service.</summary>
|
||||||
|
public const string NotifySettingsGetAll = "notify_settings_GetAll";
|
||||||
|
|
||||||
|
/// <summary>Served by FinlyticNotify: applies dynamic setting updates for the service.</summary>
|
||||||
|
public const string NotifySettingsUpdate = "notify_settings_Update";
|
||||||
|
|
||||||
// ---- FinlyticBackend ----
|
// ---- FinlyticBackend ----
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -286,6 +294,11 @@ public static class MqttTopics
|
|||||||
/// even though FinlyticBackend is outside this refactor's scope, so no future service hardcodes it again.
|
/// even though FinlyticBackend is outside this refactor's scope, so no future service hardcodes it again.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string BackendGetAggregatedFavorites = "backend_GetAggregatedFavorites";
|
public const string BackendGetAggregatedFavorites = "backend_GetAggregatedFavorites";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Served by FinlyticBackend: resolves and returns the clean username for a given UserId GUID.
|
||||||
|
/// </summary>
|
||||||
|
public const string BackendGetUsername = "backend_GetUsername";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
+24
-61
@@ -217,25 +217,37 @@ services:
|
|||||||
# See finlyticassets above: broker has no auth configured yet, do not enable.
|
# See finlyticassets above: broker has no auth configured yet, do not enable.
|
||||||
#- MQTT__Username=${MQTT_USERNAME:-admin}
|
#- MQTT__Username=${MQTT_USERNAME:-admin}
|
||||||
#- MQTT__Password=${MQTT_PASSWORD}
|
#- MQTT__Password=${MQTT_PASSWORD}
|
||||||
- MQTT__ClientId=finlytic_bot
|
|
||||||
- Alpaca__KeyId=${ALPACA_KEY_ID:-PK_PAPER_PLACEHOLDER_KEY}
|
|
||||||
- Alpaca__SecretKey=${ALPACA_SECRET_KEY:-SK_PAPER_PLACEHOLDER_SECRET}
|
|
||||||
- Alpaca__IsPaper=true
|
- Alpaca__IsPaper=true
|
||||||
|
|
||||||
|
finlyticnotify:
|
||||||
|
image: finlyticnotify
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: FinlyticNotify/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- postgres-network
|
||||||
|
environment:
|
||||||
|
- ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_notify;Username=admin;Password=${DB_PASSWORD}
|
||||||
|
- MQTT__Host=${MQTT_HOST:-host.docker.internal}
|
||||||
|
- MQTT__Port=${MQTT_PORT:-4545}
|
||||||
|
- MQTT__ClientId=finlytic_notify
|
||||||
|
- Ntfy__BaseUrl=${NTFY_BASE_URL:-http://host.docker.internal:8080}
|
||||||
|
- Ntfy__TopicPrefix=finlytic
|
||||||
|
- Ntfy__BroadcastChannel=broadcast
|
||||||
|
- Ntfy__NewsChannel=news
|
||||||
|
- Ntfy__DefaultUsername=admin
|
||||||
|
- Ntfy__MinProposalScore=70.0
|
||||||
|
- Ntfy__NotifyOnProposals=true
|
||||||
|
- Ntfy__NotifyOnTradeUpdates=true
|
||||||
|
- Ntfy__NotifyOnBotTrades=true
|
||||||
|
- Ntfy__NotifyOnNews=true
|
||||||
|
|
||||||
finlyticbackend:
|
finlyticbackend:
|
||||||
image: finlyticbackend
|
image: finlyticbackend
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: FinlyticBackend/Dockerfile
|
dockerfile: FinlyticBackend/Dockerfile
|
||||||
# on-failure (bounded), NOT unless-stopped: this is the one service that
|
|
||||||
# deliberately throws at startup if JWT_SECRET_KEY / ADMIN_DEFAULT_PASSWORD
|
|
||||||
# are missing or too weak (see Program.cs fail-fast guards). An
|
|
||||||
# unless-stopped policy would crash-loop that misconfiguration forever,
|
|
||||||
# burning CPU/log volume while masking the real problem. A bounded
|
|
||||||
# on-failure still recovers from transient startup races (e.g. DB not
|
|
||||||
# yet reachable) but eventually settles into a visibly "Exited" container
|
|
||||||
# (`docker compose ps`) once the retries are exhausted, surfacing a
|
|
||||||
# persistent config error instead of hiding it.
|
|
||||||
restart: on-failure:5
|
restart: on-failure:5
|
||||||
ports:
|
ports:
|
||||||
- "5000:8080"
|
- "5000:8080"
|
||||||
@@ -254,55 +266,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/index:/app/assets/index
|
- ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/index:/app/assets/index
|
||||||
- ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/logos:/app/assets/logos
|
- ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/logos:/app/assets/logos
|
||||||
# Honest healthcheck: actually opens a TCP connection to the real Kestrel
|
|
||||||
# port and parses the real HTTP status line from the real GET /health
|
|
||||||
# endpoint (Program.cs, AllowAnonymous, no auth required). The final image
|
|
||||||
# (mcr.microsoft.com/dotnet/aspnet:10.0) has neither curl nor wget
|
|
||||||
# installed (verified) — installing one just for this would add an extra
|
|
||||||
# apt layer, so instead we use bash's built-in /dev/tcp (bash itself IS
|
|
||||||
# present in the base image, verified), invoked directly via exec form so
|
|
||||||
# it does not go through /bin/sh (which is dash on this image and does
|
|
||||||
# NOT support /dev/tcp).
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
- CMD
|
|
||||||
- bash
|
|
||||||
- -c
|
|
||||||
- >-
|
|
||||||
exec 3<>/dev/tcp/127.0.0.1/8080 &&
|
|
||||||
printf 'GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 &&
|
|
||||||
head -n1 <&3 | grep -q '200'
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 20s
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
# No HEALTHCHECK on the 8 worker services above (finlyticassets, finlyticnews,
|
|
||||||
# finlyticfundamentals, finlyticsentiment, finlytictechnicals, finlyticengine,
|
|
||||||
# finlyticsimulation, finlyticbot) — and this is deliberate, not an omission:
|
|
||||||
#
|
|
||||||
# - They are Microsoft.NET.Sdk.Worker projects and MUST NOT host an HTTP
|
|
||||||
# server (Rules.md §5), so there is no `GET /health`-style endpoint to
|
|
||||||
# probe, by design.
|
|
||||||
# - Docker already restarts/reports a dead PID 1 via the `restart` policy
|
|
||||||
# above without any HEALTHCHECK — a HEALTHCHECK only adds value if it
|
|
||||||
# can distinguish "process alive but broken" from "process alive and
|
|
||||||
# working", which requires touching something specific to the app.
|
|
||||||
# - The only things reachable from inside these containers without an
|
|
||||||
# app-level probe endpoint are the external DB/MQTT dependencies
|
|
||||||
# themselves (e.g. via bash's /dev/tcp, as used for finlyticbackend
|
|
||||||
# above). But a bare TCP-reachability check to OmniDB/MQTT tests the
|
|
||||||
# network path, not the worker — it would report "healthy" while the
|
|
||||||
# worker is deadlocked, and "unhealthy" during a legitimate external
|
|
||||||
# outage the worker's own retry logic is already handling. That is
|
|
||||||
# placebo/misleading in both directions, not an honest signal.
|
|
||||||
#
|
|
||||||
# Conclusion: no meaningful, non-cosmetic healthcheck is possible here
|
|
||||||
# without adding an HTTP endpoint (forbidden by Rules.md §5). Leaving
|
|
||||||
# HEALTHCHECK unset is the honest choice.
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
postgres-network:
|
postgres-network:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
Reference in New Issue
Block a user