107 lines
3.1 KiB
Elixir
107 lines
3.1 KiB
Elixir
defmodule Towerops.Workers.SystemInsightWorker do
|
|
@moduledoc """
|
|
Oban cron worker that generates system-level insights.
|
|
|
|
Runs every 5 minutes to detect:
|
|
- `agent_offline`: Agents not seen in over 10 minutes
|
|
- Auto-resolves agent_offline insights when agents come back online
|
|
"""
|
|
use Oban.Pro.Worker, queue: :maintenance
|
|
|
|
import Ecto.Query
|
|
|
|
alias Towerops.Agents.AgentToken
|
|
alias Towerops.Organizations.Organization
|
|
alias Towerops.Preseem.Insight
|
|
alias Towerops.Preseem.Insights
|
|
alias Towerops.Repo
|
|
|
|
require Logger
|
|
|
|
@stale_threshold_minutes 10
|
|
|
|
@impl Oban.Pro.Worker
|
|
def process(%Oban.Job{}) do
|
|
# Use streaming to avoid loading all org IDs into memory
|
|
Repo.transaction(fn ->
|
|
Organization
|
|
|> select([o], o.id)
|
|
|> Repo.stream()
|
|
|> Enum.each(&process_organization_insights/1)
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
defp process_organization_insights(org_id) do
|
|
offline_agents = list_offline_agents(org_id)
|
|
offline_ids = MapSet.new(offline_agents, & &1.id)
|
|
|
|
Enum.each(offline_agents, &generate_agent_offline_insight_safe/1)
|
|
auto_resolve_recovered_agents(org_id, offline_ids)
|
|
end
|
|
|
|
defp generate_agent_offline_insight_safe(agent) do
|
|
case generate_agent_offline_insight(agent) do
|
|
{:ok, _} ->
|
|
:ok
|
|
|
|
{:error, changeset} ->
|
|
Logger.error("Failed to generate agent_offline insight for agent #{agent.id}: #{inspect(changeset.errors)}")
|
|
end
|
|
end
|
|
|
|
defp list_offline_agents(organization_id) do
|
|
cutoff = DateTime.add(DateTime.utc_now(), -@stale_threshold_minutes, :minute)
|
|
|
|
AgentToken
|
|
|> where([a], a.organization_id == ^organization_id)
|
|
|> where([a], a.enabled == true)
|
|
|> where([a], not is_nil(a.last_seen_at))
|
|
|> where([a], a.last_seen_at < ^cutoff)
|
|
|> Repo.all()
|
|
end
|
|
|
|
defp generate_agent_offline_insight(agent) do
|
|
minutes_offline = DateTime.diff(DateTime.utc_now(), agent.last_seen_at, :minute)
|
|
|
|
Insights.insert_insight_if_new(%{
|
|
organization_id: agent.organization_id,
|
|
agent_token_id: agent.id,
|
|
type: "agent_offline",
|
|
urgency: "critical",
|
|
channel: "proactive",
|
|
source: "system",
|
|
title: "Agent '#{agent.name}' is offline",
|
|
description:
|
|
"Agent #{agent.name} has not been seen for #{minutes_offline} minutes. " <>
|
|
"Check agent connectivity and host status.",
|
|
metadata: %{
|
|
"agent_name" => agent.name,
|
|
"last_seen_at" => DateTime.to_iso8601(agent.last_seen_at),
|
|
"minutes_offline" => minutes_offline
|
|
}
|
|
})
|
|
end
|
|
|
|
defp auto_resolve_recovered_agents(organization_id, currently_offline_ids) do
|
|
active_agent_insights =
|
|
Insight
|
|
|> where(
|
|
[i],
|
|
i.organization_id == ^organization_id and
|
|
i.type == "agent_offline" and
|
|
i.status == "active"
|
|
)
|
|
|> where([i], not is_nil(i.agent_token_id))
|
|
|> Repo.all()
|
|
|
|
Enum.each(active_agent_insights, fn insight ->
|
|
if !MapSet.member?(currently_offline_ids, insight.agent_token_id) do
|
|
insight
|
|
|> Insight.changeset(%{status: "resolved"})
|
|
|> Repo.update()
|
|
end
|
|
end)
|
|
end
|
|
end
|