defmodule Towerops.Application do @moduledoc """ OTP Application module for Towerops. Supervises the Repo, PubSub, Telemetry, Endpoint, and monitoring workers. """ use Application # Capture the build timestamp at compile time (fallback for development) @build_timestamp DateTime.utc_now() @impl true def start(_type, _args) do # Run migrations on startup (Ecto handles locking for concurrent runs) _ = if Application.get_env(:towerops, Towerops.Repo)[:database] != "towerops_test" do Towerops.Release.migrate() end topologies = Application.get_env(:libcluster, :topologies, []) children = [ {Cluster.Supervisor, [topologies, [name: Towerops.ClusterSupervisor]]}, ToweropsWeb.Telemetry, Towerops.Repo, {DNSCluster, query: Application.get_env(:towerops, :dns_cluster_query) || :ignore}, pubsub_spec(), # Start event logger (subscribes to PubSub) Towerops.Devices.EventLogger, # Start monitoring supervisor Towerops.Monitoring.Supervisor # Start a worker by calling: Towerops.Worker.start_link(arg) # {Towerops.Worker, arg}, ] ++ exq_workers() ++ [ # Start to serve requests, typically the last entry ToweropsWeb.Endpoint ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: Towerops.Supervisor] Supervisor.start_link(children, opts) end # Tell Phoenix to update the endpoint configuration # whenever the application is updated. @impl true def config_change(changed, _new, removed) do ToweropsWeb.Endpoint.config_change(changed, removed) :ok end # Configure Exq workers - don't start in test environment defp exq_workers do if Application.get_env(:towerops, :env) == :test do [] else [ {Exq, exq_config()} ] end end defp exq_config do redis_config = Application.get_env(:towerops, :redis, []) [ name: Exq, host: Keyword.get(redis_config, :host, "localhost"), port: Keyword.get(redis_config, :port, 6379), namespace: "exq", concurrency: 10, queues: ["default", "discovery", "polling", "monitoring", "maintenance"] ] end # Returns PubSub spec - uses Redis in production/clustered environments, # falls back to default PG2 adapter for Mix tasks and development defp pubsub_spec do redis_config = Application.get_env(:towerops, :redis, []) node_name = node() # Use Redis adapter only when: # 1. Redis config exists with host # 2. Node has a name (not nonode@nohost) use_redis? = Keyword.has_key?(redis_config, :host) && node_name != :nonode@nohost if use_redis? do {Phoenix.PubSub, name: Towerops.PubSub, adapter: Phoenix.PubSub.Redis, redis_config: redis_pubsub_config()} else # Default PG2 adapter for Mix tasks and development {Phoenix.PubSub, name: Towerops.PubSub} end end defp redis_pubsub_config do redis_config = Application.get_env(:towerops, :redis, []) [ host: Keyword.get(redis_config, :host, "localhost"), port: Keyword.get(redis_config, :port, 6379), node_name: node() ] end @doc """ Returns the deployment timestamp. In production (Kubernetes), this reads from the DEPLOY_TIMESTAMP environment variable which is automatically set by GitLab CI during deployment. Falls back to compile time in development. The timestamp is set by GitLab CI using: ```bash kubectl set env deployment/towerops DEPLOY_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -n towerops ``` This ensures all pods in the deployment show the same deployment time, regardless of when individual pods were created or restarted. """ @spec build_timestamp() :: DateTime.t() def build_timestamp do case System.get_env("DEPLOY_TIMESTAMP") do nil -> # Development/fallback: use compile time @build_timestamp timestamp_string -> # Production: parse from environment variable case DateTime.from_iso8601(timestamp_string) do {:ok, datetime, _offset} -> datetime {:error, _} -> @build_timestamp end end end end