Packages

appsignal

2.4.0
2.17.4 2.17.3 2.17.2 2.17.1 2.17.0 2.16.0 2.15.11 2.15.10 2.15.9 2.15.8 2.15.7 2.15.6 2.15.5 2.15.4 2.15.3 2.15.2 2.15.1 2.15.0 2.14.1 2.14.0 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.3 2.12.2 2.12.1 2.12.0 2.11.0 2.10.2 2.10.1 2.10.0 2.9.2 2.9.1 2.9.0 2.8.3 2.8.2 2.8.1 2.8.0 2.7.13 2.7.12 2.7.11 2.7.10 2.7.9 2.7.8 2.7.7 2.7.6 2.7.5 2.7.4 2.7.3 2.7.2 2.7.1 2.7.0 2.7.0-beta.1 2.6.1 2.6.0 2.5.3 2.5.2 2.5.1 2.5.0 2.4.3 2.4.2 2.4.1 2.4.0 2.3.1 2.3.0 2.2.19 2.2.18 2.2.17 2.2.16 2.2.15 2.2.14 2.2.13 2.2.12 retired 2.2.11 2.2.10 2.2.9 2.2.8 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 2.2.1 2.2.0 2.1.15 2.1.14 2.1.13 2.1.12 2.1.11 2.1.10 2.1.9 2.1.8 2.1.7 2.1.7-beta.1 2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.8 2.0.7 2.0.6 2.0.6-beta.1 2.0.5 retired 2.0.4 retired 2.0.3 retired 2.0.2 retired 2.0.1 retired 2.0.0 retired 2.0.0-beta.11 2.0.0-beta.10 2.0.0-beta.9 2.0.0-beta.8 2.0.0-beta.7 2.0.0-beta.6 2.0.0-beta.5 2.0.0-beta.4 2.0.0-beta.3 2.0.0-beta.2 2.0.0-beta.1 2.0.0-alpha.7 2.0.0-alpha.6 2.0.0-alpha.5 2.0.0-alpha.4 2.0.0-alpha.3 2.0.0-alpha.2 2.0.0-alpha.1 1.13.5 1.13.4 1.13.3 1.13.2 1.13.1 1.13.0 1.13.0-beta.2 1.13.0-beta.1 1.12.1 1.12.0 1.12.0-beta.3 1.12.0-beta.2 1.12.0-beta.1 1.11.8 1.11.7 1.11.6 1.11.5 1.11.4 1.11.3 1.11.2 1.11.1 1.11.0 1.11.0-beta.1 1.10.13 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.4 1.9.3 1.9.2 1.9.1 1.9.1-beta.1 1.9.0 1.9.0-beta.1 1.9.0-alpha.1 1.8.2 1.8.1 1.8.1-beta.2 1.8.1-beta.1 1.8.0 1.7.2 1.7.1 1.7.0 1.7.0-beta.2 1.7.0-beta.1 1.7.0-alpha.4 1.7.0-alpha.3 1.7.0-alpha.2 1.7.0-alpha.1 1.6.7 1.6.6 1.6.6-beta.2 1.6.6-beta.1 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.6.0-beta.1 1.6.0-alpha.1 1.5.1 1.5.0 1.5.0-beta.2 1.5.0-beta.1 1.4.11-beta.1 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.4.0-alpha.1 1.3.6 1.3.6-beta.1 1.3.5 1.3.4 1.3.4-beta.1 1.3.3 1.3.2 1.3.1 1.3.0 1.3.0-beta.3 1.3.0-beta.2 1.3.0-beta.1 1.2.3 1.2.2 1.2.1 1.2.0 1.1.1 1.1.0 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 1.0.0-rc.1 0.13.0 0.12.3 0.12.2 0.12.1 0.12.0 0.11.6 0.11.5 0.11.4 0.11.3 0.11.2 0.11.1 0.11.0 0.10.0 0.9.2 0.9.1 0.9.0 0.8.0 0.7.1 0.7.0 0.6.2 0.6.1 0.6.0 0.5.0 0.4.0 0.3.0 0.2.0 0.1.0 0.0.9 0.0.8 0.0.7 0.0.6 0.0.5 0.0.4 0.0.3 0.0.2 0.0.1

Collects error and performance data from your Elixir applications and sends it to AppSignal

Current section

Files

Jump to
appsignal mix_helpers.exs
Raw

mix_helpers.exs

unless Code.ensure_loaded?(Appsignal.Agent) do
{_, _} = Code.eval_file("agent.exs")
end
defmodule Mix.Appsignal.Utils do
defmacro compile_env(app, key, default \\ nil) do
if Version.match?(System.version(), ">= 1.10.0") do
quote do
Application.compile_env(unquote(app), unquote(key), unquote(default))
end
else
quote do
Application.get_env(unquote(app), unquote(key), unquote(default))
end
end
end
end
defmodule Mix.Appsignal.Helper do
@moduledoc """
Helper functions for downloading and compiling the AppSignal agent library.
"""
require Mix.Appsignal.Utils
@erlang Mix.Appsignal.Utils.compile_env(:appsignal, :erlang, :erlang)
@os Mix.Appsignal.Utils.compile_env(:appsignal, :os, :os)
@system Mix.Appsignal.Utils.compile_env(:appsignal, :mix_system, System)
@proxy_env_vars [
"APPSIGNAL_HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"http_proxy",
"HTTP_PROXY"
]
require Logger
def install do
report = initial_report()
case verify_system_architecture(report) do
{:ok, {arch, report}} ->
case find_package_source(arch, report) do
{:ok, {arch_config, %{build: %{source: "remote"}} = report}} ->
case download_and_compile(arch_config, report) do
:ok ->
Logger.debug("""
AppSignal for Elixir #{Mix.Project.config()[:version]} successfully installed!
If you're upgrading from version 1.x, please review our upgrade guide:
https://docs.appsignal.com/elixir/installation/upgrading-from-1.x-to-2.x.html
""")
{:error, {reason, report}} ->
abort_installation(reason, report)
end
{:ok, report} ->
# Installation using already downloaded package of the extension
compile(report)
{:error, {reason, report}} ->
abort_installation(reason, report)
end
{:error, {reason, report}} ->
abort_installation(reason, report)
end
end
@doc """
Checks to see if a proxy is defined in any of the accepted OS environment
variables (as per `@proxy_env_vars`).
Returns `nil` if no proxy is defined, or a `{variable_name, proxy_url}` tuple
matching the first found variable.
_NOTE: If the first variable found is defined but empty (""), proxying is
disabled (eg. `nil` is returned)._
"""
def check_proxy(environment \\ System.get_env()) do
Enum.reduce_while(@proxy_env_vars, nil, fn name, acc ->
if value = Map.get(environment, name) do
{:halt, proxy_or_nil(name, value)}
else
{:cont, acc}
end
end)
end
defp proxy_or_nil(_, ""), do: nil
defp proxy_or_nil(name, value), do: {name, value}
defp find_package_source(arch, report) do
architecture_key = arch_key(arch)
arch_config = Appsignal.Agent.triples()[architecture_key]
System.put_env("LIB_DIR", priv_dir())
cond do
has_local_release_files?() ->
Mix.shell().info("AppSignal: Using local agent release.")
File.mkdir_p!(priv_dir())
clean_up_extension_files()
Enum.each(
["appsignal.h", "appsignal-agent", "appsignal.version", "libappsignal.a"],
fn file ->
File.cp(project_ext_path(file), priv_path(file))
end
)
{:ok, merge_report(report, %{build: %{source: "local"}})}
has_files?() and has_correct_agent_version?() ->
{:ok, merge_report(report, %{build: %{source: "cached_in_priv_dir"}})}
is_nil(arch_config) ->
{:error,
{"No architecture build found for '#{architecture_key}'.",
merge_report(report, %{build: %{source: "remote"}})}}
true ->
{:ok, {arch_config, merge_report(report, %{build: %{source: "remote"}})}}
end
end
defp download_and_compile(arch_config, report) do
report = merge_report(report, %{download: %{checksum: "unverified"}})
case download_package(arch_config, report) do
{:ok, {filename, report}} ->
case verify_download_package(filename, arch_config[:checksum], report) do
{:ok, {filename, report}} ->
case extract_package(filename) do
:ok ->
compile(report)
{:error, reason} ->
{:error, {reason, report}}
end
{:error, {reason, report}} ->
{:error, {reason, report}}
end
{:error, {reason, report}} ->
{:error, {reason, report}}
end
end
defp download_package(arch_config, report) do
version = Appsignal.Agent.version()
filename = arch_config[:filename]
File.mkdir_p!(priv_dir())
clean_up_extension_files()
local_filename = Path.join(tmp_dir(), "appsignal-agent-#{version}.tar.gz")
case File.exists?(local_filename) do
true ->
{:ok, {local_filename, merge_report(report, %{build: %{source: "cached_in_tmp_dir"}})}}
false ->
Mix.shell().info("Downloading agent release")
:application.ensure_all_started(:hackney)
case do_download_file!(filename, local_filename, Appsignal.Agent.mirrors()) do
{:ok, url} ->
{:ok, {local_filename, merge_report(report, %{download: %{download_url: url}})}}
{error, url} ->
{:error, {error, merge_report(report, %{download: %{download_url: url}})}}
end
end
end
defp verify_download_package(filename, expected_checksum, report) do
data = File.read!(filename)
calculated_checksum = :crypto.hash(:sha256, data) |> Base.encode16(case: :lower)
if calculated_checksum == expected_checksum do
{:ok, {filename, merge_report(report, %{download: %{checksum: "verified"}})}}
else
sub_report = %{download: %{checksum: "invalid"}}
reason = """
Checksum verification of #{filename} failed!
Calculated: #{calculated_checksum}
Expected: #{expected_checksum}
"""
{:error, {reason, merge_report(report, sub_report)}}
end
end
defp do_download_file!(filename, local_filename, mirrors) do
Enum.reduce_while(mirrors, {1, []}, fn mirror, {acc, errors} ->
version = Appsignal.Agent.version()
url = build_download_url(mirror, version, filename)
result = do_download_file!(url, local_filename)
if result == :ok do
# Success on download
{:halt, {:ok, url}}
else
if length(mirrors) == acc do
# All mirrors failed. Write error message detailing all failures
error_messages =
Enum.map(Enum.reverse([result | errors]), fn {:error, message} ->
message
end)
message = """
Could not download archive from any of our mirrors.
Please make sure your network allows access to any of these mirrors.
Attempted to download the archive from the following urls:
#{Enum.join(error_messages, "\n")}
"""
{:halt, {String.trim(message), url}}
else
# Try the next mirror
{:cont, {acc + 1, [result | errors]}}
end
end
end)
end
defp do_download_file!(url, local_filename) do
case :hackney.request(:get, url, [], "", download_options()) do
{:ok, 200, _, reference} ->
case :hackney.body(reference) do
{:ok, body} -> File.write(local_filename, body)
{:error, reason} -> {:error, reason}
end
response ->
message = """
- URL: #{url}
- Error (hackney response):
#{inspect(response)}
"""
{:error, message}
end
end
defp build_download_url(mirror, version, filename) do
Enum.join([mirror, version, filename], "/")
end
defp download_options do
default_cacert_file_path = priv_path("cacert.pem")
cacert_file =
case check_cacert_access(default_cacert_file_path) do
:ok ->
default_cacert_file_path
{:error, message} ->
Logger.warn(
"The cacert file path: #{default_cacert_file_path} is not accessible. " <>
"Reason: #{inspect(message)}. " <>
"Using system defaults instead."
)
:certifi.cacertfile()
end
options = [
ssl_options:
[
verify: :verify_peer,
cacertfile: cacert_file
] ++ tls_options() ++ customize_hostname_check_or_verify_fun()
]
case check_proxy() do
nil ->
options
{var, url} ->
Mix.shell().info("- using proxy from #{var} (#{url})")
options ++ [proxy: url]
end
end
defp check_cacert_access(cacert_path) do
case File.stat(cacert_path) do
{:ok, %{access: access}} when access in [:read, :read_write] ->
:ok
{:ok, %{access: access}} ->
{:error, "File access is #{inspect(access)}"}
{:error, reason} ->
{:error, reason}
end
end
defp extract_package(filename) do
case System.cmd("tar", ["zxf", filename, "--no-same-owner"],
stderr_to_stdout: true,
cd: priv_dir()
) do
{_, 0} ->
:ok
{result, _exitcode} ->
IO.binwrite(result)
{:error, "Extracting of #{filename} failed!"}
end
end
defp compile(report) do
report = merge_report(report, %{build: %{agent_version: agent_version()}})
{output, exit_code} = run_make()
if exit_code == 0 do
Mix.shell().info("AppSignal extension installation successful")
write_report(merge_report(report, %{result: %{status: :success}}))
:ok
else
message = """
Build error was encountered while running `make` (exit code: #{exit_code}):
#{output}
"""
{:error, {message, report}}
end
end
defp run_make do
try do
System.cmd(make(), make_args(to_string(Mix.env())), stderr_to_stdout: true)
rescue
reason ->
{serialize_report_value(reason), 1}
end
end
defp make_args("test" <> _), do: ["-e", "CFLAGS_ADD=-DTEST"]
defp make_args(_), do: []
def verify_system_architecture(report) do
input_arch =
if force_linux_arm_build?() do
'aarch64-linux'
else
@erlang.system_info(:system_architecture)
end
case map_arch(input_arch, agent_platform()) do
{:ok, {arch, target} = architecture} ->
sub_report = %{build: %{architecture: arch, target: target}}
{:ok, {architecture, merge_report(report, sub_report)}}
{:error, {:unsupported, {arch, target}}} ->
sub_report = %{build: %{architecture: arch, target: target}}
reason =
"Unsupported target platform #{arch} - #{target}, AppSignal " <>
"integration disabled!\nPlease check " <>
"http://docs.appsignal.com/support/operating-systems.html"
{:error, {reason, merge_report(report, sub_report)}}
{:error, {:unknown, {arch, target}}} ->
sub_report = %{build: %{architecture: arch, target: target}}
reason =
"Unknown target platform #{arch} - #{target}, AppSignal " <>
"integration disabled!\nPlease check " <>
"http://docs.appsignal.com/support/operating-systems.html"
{:error, {reason, merge_report(report, sub_report)}}
end
end
if Mix.env() != :test_no_nif do
defp map_arch('i386-' ++ _, platform), do: build_for("i686", platform)
defp map_arch('i686-' ++ _, platform), do: build_for("i686", platform)
defp map_arch('x86-' ++ _, platform), do: build_for("i686", platform)
defp map_arch('amd64-' ++ _, platform), do: build_for("x86_64", platform)
defp map_arch('x86_64-' ++ _, platform), do: build_for("x86_64", platform)
defp map_arch('aarch64-' ++ _, platform), do: build_for("aarch64", platform)
defp map_arch('arm-' ++ _, platform), do: build_for("aarch64", platform)
end
defp map_arch(arch, platform), do: {:error, {:unknown, {to_string(arch), platform}}}
defp build_for(bit, platform) do
arch = {bit, platform}
case Map.has_key?(Appsignal.Agent.triples(), arch_key(arch)) do
true -> {:ok, arch}
false -> {:error, {:unsupported, arch}}
end
end
defp arch_key({arch, target}) do
"#{arch}-#{target}"
end
defp tmp_dir do
default_tmp_dir = "/tmp"
case {File.dir?(default_tmp_dir), File.stat(default_tmp_dir)} do
{true, {:ok, %{access: :write}}} -> default_tmp_dir
{true, {:ok, %{access: :read_write}}} -> default_tmp_dir
_ -> System.tmp_dir!()
end
end
defp priv_path(filename) do
Path.join(priv_dir(), filename)
end
defp project_ext_path(filename) do
Path.join([__DIR__, "c_src", filename])
end
defp has_file(filename) do
filename |> priv_path |> File.exists?()
end
defp has_local_ext_file(filename) do
filename |> project_ext_path |> File.exists?()
end
defp has_files? do
has_file("appsignal-agent") and has_file("appsignal.h") and has_file("appsignal_extension.so")
end
defp has_local_release_files? do
has_local_ext_file("appsignal-agent") and has_local_ext_file("appsignal.h") and
has_local_ext_file("libappsignal.a")
end
defp has_correct_agent_version? do
agent_version() == Appsignal.Agent.version()
end
defp agent_version do
path = priv_path("appsignal.version")
{:ok, agent_version} = File.read(path)
String.trim(agent_version)
end
defp clean_up_extension_files do
priv_dir()
|> Path.join("*appsignal*")
|> Path.wildcard()
|> Enum.each(&File.rm_rf!/1)
end
defp library_dependencies do
case ldd_version_output() do
{:ok, output} ->
case extract_ldd_version(output) do
nil ->
%{}
ldd_version ->
%{libc: ldd_version}
end
_ ->
%{}
end
end
def agent_platform do
cond do
force_linux_arm_build?() == true ->
"linux"
force_musl_build?() == true ->
"linux-musl"
true ->
case @os.type() do
{:unix, :linux} ->
agent_platform_by_ldd_version()
{_, os} ->
to_string(os)
end
end
end
defp agent_platform_by_ldd_version do
case ldd_version_output() do
{:ok, output} ->
case String.contains?(output, "musl") do
true ->
"linux-musl"
false ->
case extract_ldd_version(output) do
nil ->
"linux"
ldd_version ->
case Version.compare("#{ldd_version}.0", "2.15.0") do
:lt -> "linux-musl"
_ -> "linux"
end
end
end
_ ->
"linux"
end
end
# Fetches the libc version number from the `ldd` command
# If `ldd` is not found it returns `nil`
defp ldd_version_output do
case @system.cmd("ldd", ["--version"], stderr_to_stdout: true) do
{output, _} ->
{:ok, output}
end
rescue
exception ->
{:error, exception}
end
defp extract_ldd_version(ldd_output) when is_binary(ldd_output) do
case Regex.run(~r/\d+\.\d+/, ldd_output) do
[version | _tail] -> version
_ -> nil
end
end
defp extract_ldd_version(_), do: nil
defp initial_report do
{_, os} = :os.type()
%{
result: %{
status: :incomplete
},
language: %{
name: "elixir",
version: System.version(),
otp_version: System.otp_release()
},
build: %{
time: DateTime.to_iso8601(DateTime.utc_now()),
package_path: priv_dir(),
architecture: nil,
target: os,
musl_override: force_musl_build?(),
linux_arm_override: force_linux_arm_build?(),
library_type: "static",
dependencies: %{},
flags: %{}
},
host: %{
root_user: root?(),
dependencies: library_dependencies()
}
}
end
defp write_report(report) do
write_download_report(report)
write_report_file("install", report)
end
defp write_download_report(%{download: %{}} = report) do
%{
build: %{
time: time,
architecture: architecture,
target: target,
musl_override: musl_override,
linux_arm_override: linux_arm_override,
library_type: library_type,
dependencies: %{},
flags: %{}
}
} = report
%{download: %{checksum: checksum}} = report
download_url = Map.get(report.download, :download_url)
download_report = %{
download: %{
time: time,
architecture: architecture,
target: target,
musl_override: musl_override,
linux_arm_override: linux_arm_override,
library_type: library_type,
download_url: download_url,
checksum: checksum || "unverified"
}
}
write_report_file("download", download_report)
end
defp write_download_report(_) do
# Write nothing if no download details are recorded in the report
end
cond do
Code.ensure_loaded?(Jason) ->
defp json_encoder, do: {:ok, Jason}
Code.ensure_loaded?(Poison) ->
defp json_encoder, do: {:ok, Poison}
true ->
defp json_encoder do
{:error,
"""
No JSON encoder found. Please add jason to your list of dependencies in mix.exs:
def deps do
[
{:appsignal, "~> 1.0"},
{:jason, "~> 1.1"}
]
end
"""}
end
end
defp encode_report_file(report) do
case json_encoder() do
{:ok, encoder} -> encoder.encode(report)
{:error, reason} -> {:error, reason}
end
end
defp write_report_file(file, report) do
case encode_report_file(report) do
{:ok, body} ->
File.mkdir_p!(priv_dir())
filename = "#{file}.report"
case File.open(priv_path(filename), [:write]) do
{:ok, file} ->
result = IO.binwrite(file, body)
File.close(file)
result
{:error, reason} ->
Mix.Shell.IO.error("""
Error: Could not write AppSignal installation report file (#{filename}).
#{serialize_report_value(reason)}
""")
{:error, reason}
end
{:error, reason} ->
Mix.Shell.IO.error("""
Error: Could not encode AppSignal installation report.
#{serialize_report_value(reason)}
""")
{:error, reason}
end
end
defp merge_report(report, %{download: _download_report} = sub_report) do
key = :download
{download_report, sub_report} = Map.pop(sub_report, key)
report = Map.put(report, key, Map.merge(report[key] || %{}, download_report))
merge_report(report, sub_report)
end
defp merge_report(report, %{build: _build_report} = sub_report) do
key = :build
{build_report, sub_report} = Map.pop(sub_report, key)
report = Map.put(report, key, Map.merge(report[key], build_report))
merge_report(report, sub_report)
end
defp merge_report(report, %{result: _installation_report} = sub_report) do
key = :result
{installation_report, sub_report} = Map.pop(sub_report, key)
report = Map.put(report, key, installation_report)
merge_report(report, sub_report)
end
defp merge_report(report, %{}), do: report
defp abort_installation(reason, report) do
report =
merge_report(report, %{result: %{status: :failed, message: serialize_report_value(reason)}})
write_report(report)
Mix.Shell.IO.error("AppSignal installation failed: #{serialize_report_value(reason)}")
end
defp priv_dir do
case :code.priv_dir(:appsignal) do
{:error, :bad_name} ->
# This happens on initial compilation
Mix.Tasks.Compile.Erlang.manifests()
|> List.first()
|> Path.dirname()
|> String.trim_trailing(".mix")
|> Path.join("priv")
path ->
path
|> List.to_string()
end
end
defp force_musl_build? do
env = System.get_env("APPSIGNAL_BUILD_FOR_MUSL")
env == "1" || env == "true"
end
defp force_linux_arm_build? do
env = System.get_env("APPSIGNAL_BUILD_FOR_LINUX_ARM")
env == "1" || env == "true"
end
defp make do
if System.find_executable("gmake"), do: "gmake", else: "make"
end
if System.otp_release() >= "23" do
defp tls_options, do: [versions: :ssl.versions()[:supported]]
else
defp tls_options do
[
depth: 4,
ciphers: ciphers(),
honor_cipher_order: :undefined
]
end
if System.otp_release() >= "20.3" do
defp ciphers, do: :ssl.cipher_suites(:default, :"tlsv1.2")
else
defp ciphers, do: :ssl.cipher_suites()
end
end
if System.otp_release() >= "21" do
defp customize_hostname_check_or_verify_fun do
[
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
]
]
end
else
defp customize_hostname_check_or_verify_fun do
[
verify_fun:
{fn
_, :valid, state -> {:valid, state}
_, :valid_peer, state -> {:valid, state}
_, {:extension, _}, state -> {:unknown, state}
_, reason, _ -> {:fail, reason}
end, self()}
]
end
end
def root? do
uid() == 0
end
def uid do
try do
case @system.cmd("id", ["-u"]) do
{id, 0} ->
case Integer.parse(List.first(String.split(id, "\n"))) do
{int, _} -> int
:error -> nil
end
{_, _} ->
nil
end
catch
:error, _ ->
nil
end
end
defp serialize_report_value(value) when is_binary(value), do: value
defp serialize_report_value(value), do: inspect(value)
end