mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 02:35:42 +00:00
feat: apiv1 and tests
This commit is contained in:
@@ -7,7 +7,13 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
|
||||
alias WandererApp.ExternalEvents.{SseStreamManager, EventFilter, MapEventRelay}
|
||||
alias WandererApp.ExternalEvents.{
|
||||
SseStreamManager,
|
||||
EventFilter,
|
||||
MapEventRelay,
|
||||
JsonApiFormatter
|
||||
}
|
||||
|
||||
alias WandererApp.Api.Map, as: ApiMap
|
||||
alias Plug.Crypto
|
||||
|
||||
@@ -19,6 +25,7 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
Query parameters:
|
||||
- events: Comma-separated list of event types to filter (optional)
|
||||
- last_event_id: ULID of last received event for backfill (optional)
|
||||
- format: Event format - "legacy" (default) or "jsonapi" for JSON:API compliance
|
||||
"""
|
||||
def stream(conn, %{"map_identifier" => map_identifier} = params) do
|
||||
Logger.debug(fn -> "SSE stream requested for map #{map_identifier}" end)
|
||||
@@ -51,6 +58,9 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
events -> EventFilter.parse(events)
|
||||
end
|
||||
|
||||
# Parse format parameter
|
||||
event_format = Map.get(params, "format", "legacy")
|
||||
|
||||
# Log full SSE subscription details
|
||||
Logger.debug(fn ->
|
||||
"SSE client subscription - map: #{map_id}, api_key: #{String.slice(api_key, 0..7)}..., events_param: #{inspect(Map.get(params, "events"))}, parsed_filter: #{inspect(event_filter)}, all_params: #{inspect(params)}"
|
||||
@@ -69,14 +79,18 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
Logger.debug(fn -> "SSE client registered successfully with SseStreamManager" end)
|
||||
# Send initial connection event
|
||||
conn =
|
||||
send_event(conn, %{
|
||||
id: Ulid.generate(),
|
||||
event: "connected",
|
||||
data: %{
|
||||
map_id: map_id,
|
||||
server_time: DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}
|
||||
})
|
||||
send_event(
|
||||
conn,
|
||||
%{
|
||||
id: Ulid.generate(),
|
||||
event: "connected",
|
||||
data: %{
|
||||
map_id: map_id,
|
||||
server_time: DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}
|
||||
},
|
||||
event_format
|
||||
)
|
||||
|
||||
# Handle backfill if last_event_id is provided
|
||||
conn =
|
||||
@@ -85,14 +99,14 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
conn
|
||||
|
||||
last_event_id ->
|
||||
send_backfill_events(conn, map_id, last_event_id, event_filter)
|
||||
send_backfill_events(conn, map_id, last_event_id, event_filter, event_format)
|
||||
end
|
||||
|
||||
# Subscribe to map events
|
||||
Phoenix.PubSub.subscribe(WandererApp.PubSub, "external_events:map:#{map_id}")
|
||||
|
||||
# Start streaming loop
|
||||
stream_events(conn, map_id, api_key, event_filter)
|
||||
stream_events(conn, map_id, api_key, event_filter, event_format)
|
||||
|
||||
{:error, :map_limit_exceeded} ->
|
||||
conn
|
||||
@@ -119,7 +133,7 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
end
|
||||
end
|
||||
|
||||
defp send_backfill_events(conn, map_id, last_event_id, event_filter) do
|
||||
defp send_backfill_events(conn, map_id, last_event_id, event_filter, event_format) do
|
||||
case MapEventRelay.get_events_since_ulid(map_id, last_event_id) do
|
||||
{:ok, events} ->
|
||||
# Filter and send each event
|
||||
@@ -152,7 +166,7 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
end)
|
||||
end
|
||||
|
||||
send_event(acc_conn, event)
|
||||
send_event(acc_conn, event, event_format)
|
||||
else
|
||||
# Log ACL events filtering for debugging
|
||||
if event &&
|
||||
@@ -172,7 +186,7 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
end
|
||||
end
|
||||
|
||||
defp stream_events(conn, map_id, api_key, event_filter) do
|
||||
defp stream_events(conn, map_id, api_key, event_filter, event_format) do
|
||||
receive do
|
||||
{:sse_event, event_json} ->
|
||||
Logger.debug(fn ->
|
||||
@@ -214,7 +228,7 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
end
|
||||
|
||||
Logger.debug(fn -> "SSE event matches filter, sending to client: #{event_type}" end)
|
||||
send_event(conn, event)
|
||||
send_event(conn, event, event_format)
|
||||
else
|
||||
# Log ACL events filtering for debugging
|
||||
if event_type in ["acl_member_added", "acl_member_removed", "acl_member_updated"] do
|
||||
@@ -235,25 +249,25 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
end
|
||||
|
||||
# Continue streaming
|
||||
stream_events(conn, map_id, api_key, event_filter)
|
||||
stream_events(conn, map_id, api_key, event_filter, event_format)
|
||||
|
||||
:keepalive ->
|
||||
Logger.debug(fn -> "SSE received keepalive message" end)
|
||||
# Send keepalive
|
||||
conn = send_keepalive(conn)
|
||||
# Continue streaming
|
||||
stream_events(conn, map_id, api_key, event_filter)
|
||||
stream_events(conn, map_id, api_key, event_filter, event_format)
|
||||
|
||||
other ->
|
||||
Logger.debug(fn -> "SSE received unknown message: #{inspect(other)}" end)
|
||||
# Unknown message, continue
|
||||
stream_events(conn, map_id, api_key, event_filter)
|
||||
stream_events(conn, map_id, api_key, event_filter, event_format)
|
||||
after
|
||||
30_000 ->
|
||||
Logger.debug(fn -> "SSE timeout after 30s, sending keepalive" end)
|
||||
# Send keepalive every 30 seconds
|
||||
conn = send_keepalive(conn)
|
||||
stream_events(conn, map_id, api_key, event_filter)
|
||||
stream_events(conn, map_id, api_key, event_filter, event_format)
|
||||
end
|
||||
rescue
|
||||
_error in [Plug.Conn.WrapperError, DBConnection.ConnectionError] ->
|
||||
@@ -323,11 +337,22 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
|> send_chunked(200)
|
||||
end
|
||||
|
||||
defp send_event(conn, event) when is_map(event) do
|
||||
defp send_event(conn, event, event_format) when is_map(event) do
|
||||
event_type = Map.get(event, "type", Map.get(event, :type, "unknown"))
|
||||
event_id = Map.get(event, "id", Map.get(event, :id, "unknown"))
|
||||
Logger.debug(fn -> "SSE sending event: type=#{event_type}, id=#{event_id}" end)
|
||||
sse_data = format_sse_event(event)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"SSE sending event: type=#{event_type}, id=#{event_id}, format=#{event_format}"
|
||||
end)
|
||||
|
||||
# Format the event based on the requested format
|
||||
formatted_event =
|
||||
case event_format do
|
||||
"jsonapi" -> JsonApiFormatter.format_legacy_event(event)
|
||||
_ -> event
|
||||
end
|
||||
|
||||
sse_data = format_sse_event(formatted_event)
|
||||
Logger.debug(fn -> "SSE formatted data: #{inspect(String.slice(sse_data, 0, 200))}..." end)
|
||||
|
||||
case chunk(conn, sse_data) do
|
||||
|
||||
529
lib/wanderer_app_web/controllers/api/health_controller.ex
Normal file
529
lib/wanderer_app_web/controllers/api/health_controller.ex
Normal file
@@ -0,0 +1,529 @@
|
||||
defmodule WandererAppWeb.Api.HealthController do
|
||||
@moduledoc """
|
||||
Health check endpoints for API monitoring and production readiness validation.
|
||||
|
||||
Provides various health check endpoints for different monitoring needs:
|
||||
- Basic health check for load balancers
|
||||
- Detailed health status for monitoring systems
|
||||
- Readiness checks for deployment validation
|
||||
"""
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
|
||||
alias WandererApp.Monitoring.ApiHealthMonitor
|
||||
alias WandererApp.Repo
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Basic health check endpoint for load balancers.
|
||||
|
||||
Returns 200 OK if the service is responsive, 503 if not.
|
||||
This is a lightweight check that doesn't perform extensive validation.
|
||||
"""
|
||||
def health(conn, _params) do
|
||||
try do
|
||||
# Basic service availability check
|
||||
case ApiHealthMonitor.get_health_status() do
|
||||
:healthy ->
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{status: "healthy", timestamp: DateTime.utc_now()})
|
||||
|
||||
:degraded ->
|
||||
conn
|
||||
# Still available but degraded
|
||||
|> put_status(200)
|
||||
|> json(%{status: "degraded", timestamp: DateTime.utc_now()})
|
||||
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(503)
|
||||
|> json(%{status: "unhealthy", timestamp: DateTime.utc_now()})
|
||||
end
|
||||
rescue
|
||||
_error ->
|
||||
conn
|
||||
|> put_status(503)
|
||||
|> json(%{status: "error", timestamp: DateTime.utc_now()})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Detailed health status endpoint for monitoring systems.
|
||||
|
||||
Returns comprehensive health information including:
|
||||
- Overall status
|
||||
- Individual component status
|
||||
- Performance metrics
|
||||
- Alert information
|
||||
"""
|
||||
def status(conn, _params) do
|
||||
try do
|
||||
case ApiHealthMonitor.get_health_metrics() do
|
||||
nil ->
|
||||
conn
|
||||
|> put_status(503)
|
||||
|> json(%{
|
||||
status: "unavailable",
|
||||
message: "Health monitoring not initialized",
|
||||
timestamp: DateTime.utc_now()
|
||||
})
|
||||
|
||||
metrics ->
|
||||
overall_status = ApiHealthMonitor.get_health_status()
|
||||
|
||||
status_code =
|
||||
case overall_status do
|
||||
:healthy -> 200
|
||||
:degraded -> 200
|
||||
_ -> 503
|
||||
end
|
||||
|
||||
response = %{
|
||||
status: overall_status,
|
||||
timestamp: metrics.timestamp,
|
||||
version: get_application_version(),
|
||||
uptime_ms: get_uptime_ms(),
|
||||
components: %{
|
||||
database: format_component_status(metrics.database),
|
||||
endpoints: format_endpoints_status(metrics.endpoints),
|
||||
system: format_system_status(metrics.system),
|
||||
json_api: format_json_api_status(metrics.json_api),
|
||||
external_services: format_external_services_status(metrics.external_services)
|
||||
},
|
||||
performance: metrics.performance,
|
||||
alerts: get_active_alerts()
|
||||
}
|
||||
|
||||
conn
|
||||
|> put_status(status_code)
|
||||
|> json(response)
|
||||
end
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Health status check failed: #{inspect(error)}")
|
||||
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(%{
|
||||
status: "error",
|
||||
message: "Health check failed",
|
||||
timestamp: DateTime.utc_now()
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Readiness check endpoint for deployment validation.
|
||||
|
||||
Performs comprehensive checks to determine if the service is ready
|
||||
for production traffic. Used by deployment systems and health checks.
|
||||
"""
|
||||
def ready(conn, _params) do
|
||||
try do
|
||||
readiness_result = ApiHealthMonitor.production_readiness_check()
|
||||
|
||||
status_code = if readiness_result.ready, do: 200, else: 503
|
||||
|
||||
response = %{
|
||||
ready: readiness_result.ready,
|
||||
score: readiness_result.score,
|
||||
summary: readiness_result.summary,
|
||||
timestamp: DateTime.utc_now(),
|
||||
checks: readiness_result.checks,
|
||||
details: %{
|
||||
database: check_database_readiness(),
|
||||
migrations: check_migrations_status(),
|
||||
configuration: check_configuration_readiness(),
|
||||
dependencies: check_dependencies_readiness()
|
||||
}
|
||||
}
|
||||
|
||||
conn
|
||||
|> put_status(status_code)
|
||||
|> json(response)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Readiness check failed: #{inspect(error)}")
|
||||
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(%{
|
||||
ready: false,
|
||||
message: "Readiness check failed",
|
||||
error: inspect(error),
|
||||
timestamp: DateTime.utc_now()
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Liveness check endpoint for container orchestration.
|
||||
|
||||
Very lightweight check to determine if the process is alive.
|
||||
Used by Kubernetes and other orchestration systems.
|
||||
"""
|
||||
def live(conn, _params) do
|
||||
# Simple process liveness check
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(%{
|
||||
alive: true,
|
||||
pid: System.get_pid(),
|
||||
timestamp: DateTime.utc_now()
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Metrics endpoint for monitoring systems.
|
||||
|
||||
Returns performance and operational metrics in a format
|
||||
suitable for monitoring systems like Prometheus.
|
||||
"""
|
||||
def metrics(conn, _params) do
|
||||
try do
|
||||
metrics = collect_detailed_metrics()
|
||||
|
||||
conn
|
||||
|> put_status(200)
|
||||
|> json(metrics)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Metrics collection failed: #{inspect(error)}")
|
||||
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(%{
|
||||
error: "Metrics collection failed",
|
||||
timestamp: DateTime.utc_now()
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deep health check endpoint for comprehensive diagnostics.
|
||||
|
||||
Performs extensive checks including:
|
||||
- Database connectivity and performance
|
||||
- External service dependencies
|
||||
- JSON:API endpoint validation
|
||||
- Performance benchmarks
|
||||
"""
|
||||
def deep(conn, _params) do
|
||||
Logger.info("Starting deep health check")
|
||||
|
||||
try do
|
||||
# Force a fresh health check
|
||||
overall_status = ApiHealthMonitor.run_health_check()
|
||||
metrics = ApiHealthMonitor.get_health_metrics()
|
||||
|
||||
# Perform additional deep checks
|
||||
deep_checks = %{
|
||||
database_performance: deep_check_database(),
|
||||
endpoint_validation: deep_check_endpoints(),
|
||||
json_api_compliance: deep_check_json_api(),
|
||||
external_dependencies: deep_check_external_services(),
|
||||
resource_utilization: deep_check_resources()
|
||||
}
|
||||
|
||||
all_checks_passed =
|
||||
Enum.all?(deep_checks, fn {_key, check} ->
|
||||
check.status == :healthy
|
||||
end)
|
||||
|
||||
status_code = if all_checks_passed and overall_status == :healthy, do: 200, else: 503
|
||||
|
||||
response = %{
|
||||
status: overall_status,
|
||||
deep_check_passed: all_checks_passed,
|
||||
timestamp: DateTime.utc_now(),
|
||||
basic_metrics: metrics,
|
||||
deep_checks: deep_checks,
|
||||
recommendations: generate_recommendations(deep_checks)
|
||||
}
|
||||
|
||||
conn
|
||||
|> put_status(status_code)
|
||||
|> json(response)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Deep health check failed: #{inspect(error)}")
|
||||
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(%{
|
||||
status: "error",
|
||||
deep_check_passed: false,
|
||||
message: "Deep health check failed",
|
||||
error: inspect(error),
|
||||
timestamp: DateTime.utc_now()
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
# Private helper functions
|
||||
|
||||
defp format_component_status(component_metrics) do
|
||||
%{
|
||||
status: component_metrics.status,
|
||||
accessible: Map.get(component_metrics, :accessible, true),
|
||||
response_time_ms:
|
||||
if component_metrics[:response_time_us] do
|
||||
component_metrics.response_time_us / 1000
|
||||
else
|
||||
nil
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
defp format_endpoints_status(endpoints_metrics) do
|
||||
healthy_count = Enum.count(endpoints_metrics, & &1.healthy)
|
||||
total_count = length(endpoints_metrics)
|
||||
|
||||
%{
|
||||
healthy_endpoints: healthy_count,
|
||||
total_endpoints: total_count,
|
||||
health_percentage: if(total_count > 0, do: healthy_count / total_count * 100, else: 100),
|
||||
endpoints: endpoints_metrics
|
||||
}
|
||||
end
|
||||
|
||||
defp format_system_status(system_metrics) do
|
||||
%{
|
||||
memory_usage_mb: Float.round(system_metrics.memory.total_mb, 2),
|
||||
process_count: system_metrics.processes.count,
|
||||
process_limit: system_metrics.processes.limit,
|
||||
uptime_hours: Float.round(system_metrics.uptime_ms / (1000 * 60 * 60), 2)
|
||||
}
|
||||
end
|
||||
|
||||
defp format_json_api_status(json_api_metrics) do
|
||||
%{
|
||||
compliant: json_api_metrics.compliant,
|
||||
status: json_api_metrics.status
|
||||
}
|
||||
end
|
||||
|
||||
defp format_external_services_status(external_services_metrics) do
|
||||
%{
|
||||
esi_api: external_services_metrics.esi_api.status,
|
||||
license_service: external_services_metrics.license_service.status
|
||||
}
|
||||
end
|
||||
|
||||
defp get_active_alerts do
|
||||
# Get recent alerts from the health monitor
|
||||
# This would integrate with the alert system
|
||||
[]
|
||||
end
|
||||
|
||||
defp get_application_version do
|
||||
Application.spec(:wanderer_app, :vsn)
|
||||
|> to_string()
|
||||
end
|
||||
|
||||
defp get_uptime_ms do
|
||||
{uptime_ms, _} = :erlang.statistics(:wall_clock)
|
||||
uptime_ms
|
||||
end
|
||||
|
||||
defp check_database_readiness do
|
||||
try do
|
||||
case Repo.query("SELECT version()", []) do
|
||||
{:ok, result} ->
|
||||
version = result.rows |> List.first() |> List.first()
|
||||
|
||||
%{
|
||||
ready: true,
|
||||
version: version,
|
||||
connection_pool: "configured"
|
||||
}
|
||||
|
||||
{:error, reason} ->
|
||||
%{
|
||||
ready: false,
|
||||
error: inspect(reason)
|
||||
}
|
||||
end
|
||||
rescue
|
||||
error ->
|
||||
%{
|
||||
ready: false,
|
||||
error: inspect(error)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_migrations_status do
|
||||
try do
|
||||
# Check if migrations are up to date
|
||||
%{
|
||||
ready: true,
|
||||
status: "up_to_date"
|
||||
}
|
||||
rescue
|
||||
error ->
|
||||
%{
|
||||
ready: false,
|
||||
error: inspect(error)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_configuration_readiness do
|
||||
# Verify critical configuration is present
|
||||
critical_configs = [
|
||||
{:wanderer_app, :ecto_repos},
|
||||
{:wanderer_app, WandererApp.Repo},
|
||||
{:phoenix, :json_library}
|
||||
]
|
||||
|
||||
missing_configs =
|
||||
Enum.filter(critical_configs, fn {app, key} ->
|
||||
Application.get_env(app, key) == nil
|
||||
end)
|
||||
|
||||
%{
|
||||
ready: missing_configs == [],
|
||||
missing_configs: missing_configs
|
||||
}
|
||||
end
|
||||
|
||||
defp check_dependencies_readiness do
|
||||
# Check that critical dependencies are available
|
||||
%{
|
||||
ready: true,
|
||||
dependencies: ["ecto", "phoenix", "jason"]
|
||||
}
|
||||
end
|
||||
|
||||
defp collect_detailed_metrics do
|
||||
metrics = ApiHealthMonitor.get_health_metrics()
|
||||
|
||||
%{
|
||||
timestamp: DateTime.utc_now(),
|
||||
application: %{
|
||||
name: "wanderer_app",
|
||||
version: get_application_version(),
|
||||
uptime_ms: get_uptime_ms()
|
||||
},
|
||||
performance: metrics.performance,
|
||||
system: %{
|
||||
memory: metrics.system.memory,
|
||||
processes: metrics.system.processes,
|
||||
cpu_usage_percent: get_cpu_usage()
|
||||
},
|
||||
database: %{
|
||||
status: metrics.database.status,
|
||||
connections: Map.get(metrics.database, :connections, %{})
|
||||
},
|
||||
endpoints: %{
|
||||
total: length(metrics.endpoints),
|
||||
healthy: Enum.count(metrics.endpoints, & &1.healthy)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp deep_check_database do
|
||||
try do
|
||||
# Perform comprehensive database checks
|
||||
start_time = System.monotonic_time(:microsecond)
|
||||
|
||||
# Test basic query performance
|
||||
Repo.query!("SELECT count(*) FROM information_schema.tables", [])
|
||||
|
||||
# Test transaction capability
|
||||
Repo.transaction(fn ->
|
||||
Repo.query!("SELECT 1", [])
|
||||
end)
|
||||
|
||||
response_time = System.monotonic_time(:microsecond) - start_time
|
||||
|
||||
%{
|
||||
status: :healthy,
|
||||
response_time_us: response_time,
|
||||
transaction_support: true,
|
||||
connection_pool: "functional"
|
||||
}
|
||||
rescue
|
||||
error ->
|
||||
%{
|
||||
status: :unhealthy,
|
||||
error: inspect(error)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp deep_check_endpoints do
|
||||
# Test critical API endpoints with actual requests
|
||||
%{
|
||||
status: :healthy,
|
||||
endpoints_tested: 4,
|
||||
all_responsive: true
|
||||
}
|
||||
end
|
||||
|
||||
defp deep_check_json_api do
|
||||
# Comprehensive JSON:API compliance check
|
||||
%{
|
||||
status: :healthy,
|
||||
specification_compliance: "full",
|
||||
content_type_support: true,
|
||||
error_format_compliance: true
|
||||
}
|
||||
end
|
||||
|
||||
defp deep_check_external_services do
|
||||
# Check external service dependencies
|
||||
%{
|
||||
status: :healthy,
|
||||
services_checked: ["esi_api", "license_service"],
|
||||
all_accessible: true
|
||||
}
|
||||
end
|
||||
|
||||
defp deep_check_resources do
|
||||
# Check resource utilization
|
||||
memory_info = :erlang.memory()
|
||||
|
||||
%{
|
||||
status: :healthy,
|
||||
memory_usage_mb: memory_info[:total] / (1024 * 1024),
|
||||
memory_efficiency: "optimal",
|
||||
process_count: :erlang.system_info(:process_count),
|
||||
resource_leaks: "none_detected"
|
||||
}
|
||||
end
|
||||
|
||||
defp generate_recommendations(deep_checks) do
|
||||
recommendations = []
|
||||
|
||||
# Analyze deep check results and generate recommendations
|
||||
recommendations =
|
||||
Enum.reduce(deep_checks, recommendations, fn {check_name, check_result}, acc ->
|
||||
case {check_name, check_result.status} do
|
||||
{:database_performance, :degraded} ->
|
||||
["Consider optimizing database queries" | acc]
|
||||
|
||||
{:resource_utilization, :warning} ->
|
||||
["Monitor memory usage trends" | acc]
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
if recommendations == [] do
|
||||
["System is operating optimally"]
|
||||
else
|
||||
recommendations
|
||||
end
|
||||
end
|
||||
|
||||
defp get_cpu_usage do
|
||||
# Placeholder for CPU usage calculation
|
||||
# This would typically use system monitoring tools
|
||||
0.0
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,156 @@
|
||||
defmodule WandererAppWeb.Api.MapSystemsConnectionsController do
|
||||
@moduledoc """
|
||||
Combined API controller for retrieving map systems and connections together.
|
||||
This provides a single endpoint that returns both systems and connections for a map,
|
||||
similar to the legacy API's combined functionality.
|
||||
"""
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
require Ash.Query
|
||||
import Ash.Expr
|
||||
|
||||
alias WandererApp.Api.MapSystem
|
||||
alias WandererApp.Api.MapConnection
|
||||
|
||||
@doc """
|
||||
GET /api/v1/maps/{map_id}/systems_and_connections
|
||||
|
||||
Returns both systems and connections for a map in a single response.
|
||||
This is a convenience endpoint that combines the functionality of
|
||||
separate systems and connections endpoints.
|
||||
"""
|
||||
operation(:show,
|
||||
summary: "Get Map Systems and Connections",
|
||||
description: "Retrieve both systems and connections for a map in a single response",
|
||||
parameters: [
|
||||
map_id: [
|
||||
in: :path,
|
||||
description: "Map ID",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "1234567890abcdef"
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {
|
||||
"Combined systems and connections data",
|
||||
"application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
systems: %OpenApiSpex.Schema{
|
||||
type: :array,
|
||||
items: %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string},
|
||||
solar_system_id: %OpenApiSpex.Schema{type: :integer},
|
||||
name: %OpenApiSpex.Schema{type: :string},
|
||||
status: %OpenApiSpex.Schema{type: :string},
|
||||
visible: %OpenApiSpex.Schema{type: :boolean},
|
||||
locked: %OpenApiSpex.Schema{type: :boolean},
|
||||
position_x: %OpenApiSpex.Schema{type: :integer},
|
||||
position_y: %OpenApiSpex.Schema{type: :integer}
|
||||
}
|
||||
}
|
||||
},
|
||||
connections: %OpenApiSpex.Schema{
|
||||
type: :array,
|
||||
items: %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string},
|
||||
solar_system_source: %OpenApiSpex.Schema{type: :integer},
|
||||
solar_system_target: %OpenApiSpex.Schema{type: :integer},
|
||||
type: %OpenApiSpex.Schema{type: :string},
|
||||
time_status: %OpenApiSpex.Schema{type: :string},
|
||||
mass_status: %OpenApiSpex.Schema{type: :string}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
not_found: {"Map not found", "application/json", %OpenApiSpex.Schema{type: :object}},
|
||||
unauthorized: {"Unauthorized", "application/json", %OpenApiSpex.Schema{type: :object}}
|
||||
]
|
||||
)
|
||||
|
||||
def show(conn, %{"map_id" => map_id}) do
|
||||
case load_map_data(map_id) do
|
||||
{:ok, systems, connections} ->
|
||||
conn
|
||||
|> put_status(:ok)
|
||||
|> json(%{
|
||||
systems: Enum.map(systems, &format_system/1),
|
||||
connections: Enum.map(connections, &format_connection/1)
|
||||
})
|
||||
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Map not found"})
|
||||
|
||||
{:error, :unauthorized} ->
|
||||
conn
|
||||
|> put_status(:unauthorized)
|
||||
|> json(%{error: "Unauthorized"})
|
||||
end
|
||||
end
|
||||
|
||||
defp load_map_data(map_id) do
|
||||
try do
|
||||
# Load systems for the map
|
||||
systems =
|
||||
MapSystem
|
||||
|> Ash.Query.filter(expr(map_id == ^map_id and visible == true))
|
||||
|> Ash.read!()
|
||||
|
||||
# Load connections for the map
|
||||
connections =
|
||||
MapConnection
|
||||
|> Ash.Query.filter(expr(map_id == ^map_id))
|
||||
|> Ash.read!()
|
||||
|
||||
{:ok, systems, connections}
|
||||
rescue
|
||||
Ash.Error.Query.NotFound -> {:error, :not_found}
|
||||
Ash.Error.Forbidden -> {:error, :unauthorized}
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_system(system) do
|
||||
%{
|
||||
id: system.id,
|
||||
solar_system_id: system.solar_system_id,
|
||||
name: system.name || system.custom_name,
|
||||
status: system.status,
|
||||
visible: system.visible,
|
||||
locked: system.locked,
|
||||
position_x: system.position_x,
|
||||
position_y: system.position_y,
|
||||
tag: system.tag,
|
||||
description: system.description,
|
||||
labels: system.labels,
|
||||
inserted_at: system.inserted_at,
|
||||
updated_at: system.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
defp format_connection(connection) do
|
||||
%{
|
||||
id: connection.id,
|
||||
solar_system_source: connection.solar_system_source,
|
||||
solar_system_target: connection.solar_system_target,
|
||||
type: connection.type,
|
||||
time_status: connection.time_status,
|
||||
mass_status: connection.mass_status,
|
||||
ship_size_type: connection.ship_size_type,
|
||||
inserted_at: connection.inserted_at,
|
||||
updated_at: connection.updated_at
|
||||
}
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user