defmodule Mix.Tasks.Appsignal.Install do
use Mix.Task
@shortdoc "Installs AppSignal into the current application"
def run(args) do
case :os.type() do
{:win32, _} ->
"""
We've detected that you're running Windows. Unfortunately, AppSignal does not support Windows at this time.
"""
|> IO.puts()
exit(:shutdown)
_ ->
do_run(args)
end
end
defp do_run([]) do
header()
"""
We're missing an AppSignal Push API key and cannot continue.
Please supply one as an argument to this command.
mix appsignal.install push_api_key
You can find your push_api_key on https://appsignal.com/accounts under 'Add app'
Contact us at support@appsignal.com if you're stuck.
"""
|> IO.puts()
end
defp do_run([push_api_key]) do
config = %{otp_app: otp_app(), active: true, push_api_key: push_api_key, request_headers: []}
Application.put_env(:appsignal, :config, config)
Appsignal.Config.initialize()
header()
validate_push_api_key()
config = Map.put(config, :name, ask_for_app_name(config))
case ask_kind_of_configuration() do
:file ->
write_config_file(config)
link_config_file()
activate_config_for_env("dev")
activate_config_for_env("stag")
activate_config_for_env("prod")
:env ->
output_config_environment_variables(config)
end
if Code.ensure_loaded?(Phoenix) do
output_phoenix_instructions()
end
IO.puts("\nAppSignal installed! 🎉")
Mix.Tasks.Appsignal.Demo.run([])
end
defp otp_app do
config = Mix.Project.config()
Keyword.get(config, :app)
end
defp header do
"""
AppSignal install
#{hr()}
Website: https://appsignal.com
Documentation: http://docs.appsignal.com
Support: support@appsignal.com
#{hr()}
Welcome to AppSignal!
This installer will guide you through setting up AppSignal in your application.
We will perform some checks on your system and ask how you like AppSignal to be
configured.
#{hr()}
"""
|> IO.puts()
end
defp hr, do: String.duplicate("=", 80)
defp validate_push_api_key do
IO.write("Validating Push API key: ")
config = Application.get_env(:appsignal, :config)
case Appsignal.Utils.PushApiKeyValidator.validate(config) do
:ok ->
IO.puts("Valid! 🎉")
{:error, :invalid} ->
print_invalid()
exit(:shutdown)
{:error, reason} ->
print_validating_error(reason)
exit(:shutdown)
end
end
defp print_invalid do
"""
Invalid
Please make sure you're using the correct push api key from appsignal.com
Contact us at support@appsignal.com if you're stuck.
"""
|> IO.puts()
end
defp print_validating_error(reason) do
"""
Validating failed, reason:
#{inspect(reason)}
"""
|> IO.puts()
end
defp ask_for_app_name(%{otp_app: default}) do
name = ask_for_input("What is your application's name? [#{default}]")
if String.length(name) < 1 do
default
else
name
end
end
defp ask_kind_of_configuration do
"""
There are two methods of configuring AppSignal in your application.
Option 1: Using a "config/appsignal.exs" file. (1)
Option 2: Using system environment variables. (2)
"""
|> IO.puts()
case ask_for_input("What is your preferred configuration method? [1]") do
"1" ->
:file
"2" ->
:env
input ->
if String.length(input) < 1 do
:file
else
IO.puts("I'm sorry, I didn't quite get that. Please choose option 1 or 2.")
ask_kind_of_configuration()
end
end
end
defp output_config_environment_variables(config) do
"""
Configuring with environment variables.
Please put the following variables in your environment to configure AppSignal.
export APPSIGNAL_OTP_APP="#{config[:otp_app]}"
export APPSIGNAL_APP_NAME="#{config[:name]}"
export APPSIGNAL_APP_ENV="prod"
export APPSIGNAL_PUSH_API_KEY="#{config[:push_api_key]}"
"""
|> IO.puts()
end
defp write_config_file(config) do
IO.write("Writing config file config/appsignal.exs: ")
File.mkdir_p("config")
case File.open(appsignal_config_file_path(), [:write]) do
{:ok, file} ->
case IO.binwrite(file, appsignal_config_file_contents(config)) do
:ok ->
IO.puts("Success!")
{:error, reason} ->
IO.puts("Failure! #{inspect(reason)}")
exit(:shutdown)
end
File.close(file)
{:error, reason} ->
IO.puts("Failure! #{inspect(reason)}")
exit(:shutdown)
end
end
# Link the config/appsignal.exs config file to the config/config.exs file.
# If already linked, it's ignored.
defp link_config_file do
IO.write("Linking config to config/config.exs: ")
active_content = "\nimport_config \"#{appsignal_config_filename()}\"\n"
cond do
appsignal_config_linked?() ->
IO.puts("Success! (Already linked?)")
File.exists?(config_file_path()) ->
case append_to_file(config_file_path(), active_content) do
:ok ->
IO.puts("Success!")
{:error, reason} ->
IO.puts("Failure! #{inspect(reason)}")
exit(:shutdown)
end
true ->
case File.write(config_file_path(), "import Config\n#{active_content}") do
:ok ->
IO.puts("Success!")
{:error, reason} ->
IO.puts("Failure! #{inspect(reason)}")
exit(:shutdown)
end
end
end
# Checks if AppSignal was already linked in the main config/config.exs file.
defp appsignal_config_linked? do
case File.read(config_file_path()) do
{:ok, contents} ->
String.contains?(contents, ~s(import_config "#{appsignal_config_filename()})) ||
String.contains?(contents, "import_config '#{appsignal_config_filename()}")
{:error, :enoent} ->
false
end
end
# Contents for the config/appsignal.exs file.
defp appsignal_config_file_contents(config) do
options = """
otp_app: #{inspect(config[:otp_app])},
name: "#{config[:name]}",
push_api_key: "#{config[:push_api_key]}",
env: Mix.env
"""
options_with_active =
case has_environment_configuration_files?() do
false -> " active: true,\n" <> options
true -> options
end
"""
import Config
config :appsignal, :config,
#{options_with_active}
"""
end
# Append a line to Mix configuration environment files which activate
# AppSignal. This is done for development, staging and production
# environments if they are present.
defp activate_config_for_env(env) do
env_file = config_path_for_env(env)
if File.exists?(env_file) do
IO.write("Activating #{env} environment: ")
active_content = "\nconfig :appsignal, :config, active: true\n"
case file_contains?(env_file, active_content) do
:ok ->
IO.puts("Success! (Already active?)")
{:error, :not_found} ->
case append_to_file(env_file, active_content) do
:ok ->
IO.puts("Success!")
{:error, reason} ->
IO.puts("Failure! #{inspect(reason)}")
exit(:shutdown)
end
{:error, reason} ->
IO.puts("Failure! #{inspect(reason)}")
exit(:shutdown)
end
end
end
defp file_contains?(path, contents) do
case File.read(path) do
{:ok, file_contents} ->
case String.contains?(file_contents, contents) do
true -> :ok
_ -> {:error, :not_found}
end
{:error, reason} ->
{:error, reason}
end
end
defp append_to_file(path, contents) do
case File.open(path, [:append]) do
{:ok, file} ->
result = IO.binwrite(file, contents)
File.close(file)
result
{:error, reason} ->
{:error, reason}
end
end
defp output_phoenix_instructions do
"""
AppSignal detected a Phoenix app
Please follow the following guide to integrate AppSignal in your
Phoenix application.
http://docs.appsignal.com/elixir/integrations/phoenix.html
"""
|> IO.puts()
end
defp ask_for_input(prompt) do
String.trim(IO.gets("#{prompt}: "))
end
defp has_environment_configuration_files? do
"dev" |> config_path_for_env |> File.exists?() or
"stag" |> config_path_for_env |> File.exists?() or
"prod" |> config_path_for_env |> File.exists?()
end
defp appsignal_config_filename, do: "appsignal.exs"
defp config_file_path, do: Path.join("config", "config.exs")
defp appsignal_config_file_path, do: Path.join("config", appsignal_config_filename())
defp config_path_for_env(env), do: Path.join("config", "#{env}.exs")
end