Packages

appsignal

2.0.0-alpha.5
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

{_, _} = Code.eval_file("agent.exs")
defmodule Mix.Appsignal.Helper do
@moduledoc """
Helper functions for downloading and compiling the AppSignal agent library.
"""
@os Application.get_env(:appsignal, :os, :os)
@system Application.get_env(:appsignal, :mix_system, System)
@max_retries 5
@proxy_env_vars [
"APPSIGNAL_HTTP_PROXY",
"https_proxy",
"HTTPS_PROXY",
"http_proxy",
"HTTP_PROXY"
]
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}} ->
download_and_compile(arch_config, report)
{: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 enviroment
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} ->
abort_installation(reason, report)
end
{:error, {reason, report}} ->
abort_installation(reason, report)
end
{:error, {reason, report}} ->
abort_installation(reason, report)
end
end
defp download_package(arch_config, report) do
version = Appsignal.Agent.version()
File.mkdir_p!(priv_dir())
clean_up_extension_files()
url = arch_config[:download_url]
report = merge_report(report, %{download: %{download_url: url}})
filename = Path.join(tmp_dir(), "appsignal-agent-#{version}.tar.gz")
case File.exists?(filename) do
true ->
{:ok, {filename, merge_report(report, %{build: %{source: "cached_in_tmp_dir"}})}}
false ->
Mix.shell().info("Downloading agent release from #{url}")
:application.ensure_all_started(:hackney)
case do_download_file!(url, filename, @max_retries) do
:ok ->
{:ok, {filename, report}}
error ->
{:error, {error, report}}
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!(url, filename, retries \\ 0)
defp do_download_file!(url, filename, 0) do
case :hackney.request(:get, url, [], "", download_options()) do
{:ok, 200, _, reference} ->
case :hackney.body(reference) do
{:ok, body} -> File.write(filename, body)
{:error, reason} -> {:error, reason}
end
response ->
{:error, response}
end
end
defp do_download_file!(url, filename, retries) do
case do_download_file!(url, filename) do
:ok -> :ok
_ -> do_download_file!(url, filename, retries - 1)
end
end
defp download_options do
options = [
ssl_options:
[
verify: :verify_peer,
cacertfile: priv_path("cacert.pem"),
depth: 4,
ciphers: ciphers(),
honor_cipher_order: :undefined
] ++ 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 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}
"""
abort_installation(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: []
defp verify_system_architecture(report) do
input_arch = :erlang.system_info(:system_architecture)
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)
end
defp map_arch(arch, platform), do: {:error, {:unknown, {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
case force_musl_build?() do
true ->
"linux-musl"
false ->
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?(),
library_type: "static"
},
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,
library_type: library_type
}
} = report
%{
download: %{
download_url: download_url,
checksum: checksum
}
} = report
download_report = %{
download: %{
time: time,
architecture: architecture,
target: target,
musl_override: musl_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
!is_nil(System.get_env("APPSIGNAL_BUILD_FOR_MUSL"))
end
defp make do
if System.find_executable("gmake"), do: "gmake", else: "make"
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
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