Elixir: スタックトレースを取得する
Tags: elixir, webdev, tutorial, programming
Published: 2019-02-05 01:25:18

---
title: Elixir: スタックトレースを取得する
published: true
description: Elixirで例外が起きたときだけでなく、普通の場合も含めて、スタックトレースの情報の取り出し方についてご説明します。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿は「[Elixirでスタックトレースを取得する](https://qiita.com/melpon/items/03884ef46412d38bbea0)」をもとに加筆・補正し、文章を整えました。Elixirで例外が起きたときだけでなく、普通の場合も含めて、スタックトレースの情報の取り出し方についてご説明します。


# 例外が起きたとき
例外が起きたときには当然、詳細なログを出すためにスタックトレース情報が必要です。例外時のスタックトレースの情報は、Elixir 1.7に備わった[`__STACKTRACE__/0 `](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#__STACKTRACE__/0)で得られます。それより前のバージョンでは、[`System.stacktrace/0`](https://hexdocs.pm/elixir/System.html#stacktrace/0)をお使いください。ただし、v1.7以降で用いることは推奨されません(「[Elixir v1.7がリリースされた](https://dev.to/gumi/elixir-v17-1ohl)」「\_\_STACKTRACE\_\_構造体」参照)。たとえば、つぎのコードを`test.exs`に書いてみましょう。[^1]

```elixir
defmodule X do
  def f() do
    throw 42
  end
  def g() do
    f() + 1
  end
  def h() do
    g() + 1
  end
end

try do
  X.h()
catch
  42 ->
    # IO.inspect System.stacktrace()  # v1.7より前に対応させたいとき
    IO.inspect __STACKTRACE__  # v1.7以降
end
```

コマンドラインツールに`iex test.exs`と打ち込むと、スタックトレース情報が出力されます。

```terminal
[
  {X, :f, 0, [file: 'test.exs', line: 3]},
  {X, :g, 0, [file: 'test.exs', line: 6]},
  {X, :h, 0, [file: 'test.exs', line: 9]},
  {:elixir_compiler_0, :__FILE__, 1, [file: 'test.exs', line: 14]},
  {:elixir_compiler, :dispatch, 4, [file: 'src/elixir_compiler.erl', line: 79]},
  {:elixir_compiler, :compile, 3, [file: 'src/elixir_compiler.erl', line: 63]},
  {:elixir_lexical, :run, 2, [file: 'src/elixir_lexical.erl', line: 17]},
  {:elixir_compiler, :quoted, 2, [file: 'src/elixir_compiler.erl', line: 23]}
]
```

`__STACKTRACE__/0`の戻り値をさらに[`Exception.format_stacktrace/1`](https://hexdocs.pm/elixir/Exception.html#format_stacktrace/1)に渡せば、出力が整形されます。

```elixir
try do
  X.h()
catch
  42 ->
    # IO.inspect __STACKTRACE__
    IO.puts Exception.format_stacktrace(__STACKTRACE__)
end
```

```terminal
test.exs:3: X.f/0
test.exs:6: X.g/0
test.exs:9: X.h/0
test.exs:14: (file)
(elixir) src/elixir_compiler.erl:79: :elixir_compiler.dispatch/4
(elixir) src/elixir_compiler.erl:63: :elixir_compiler.compile/3
(elixir) src/elixir_lexical.erl:17: :elixir_lexical.run/2
(elixir) src/elixir_compiler.erl:23: :elixir_compiler.quoted/2
```


# 通常のとき
例外が起きていなくても、関数がどこから呼ばれたか確かめたいことはあります。たとえば、共通処理の関数内で`Logger.info`によりログを出力するとき、スタックトレースの情報があれば、問題はすぐにつきとめられるでしょう。

通常のときにスタックトレースの情報を取得するのは、[`Process.info/2`](https://hexdocs.pm/elixir/Process.html#info/2)です。関数には、つぎのように現在のプロセスを与えます。

```elixir
defmodule X do
  def f() do
    {:current_stacktrace, t} = Process.info(self(), :current_stacktrace)
    IO.inspect t
    0
  end
  def g() do
    f() + 1
  end
  def h() do
    g() + 1
  end
end

X.h() + 1
```

```terminal
[
  {Process, :info, 2, [file: 'lib/process.ex', line: 767]},
  {X, :f, 0, [file: 'test.exs', line: 3]},
  {X, :g, 0, [file: 'test.exs', line: 8]},
  {X, :h, 0, [file: 'test.exs', line: 11]},
  {:elixir_compiler_0, :__FILE__, 1, [file: 'test.exs', line: 15]},
  {:elixir_compiler, :dispatch, 4, [file: 'src/elixir_compiler.erl', line: 79]},
  {:elixir_compiler, :compile, 3, [file: 'src/elixir_compiler.erl', line: 63]}
]
```

例外時と同じく、`Exception.format_stacktrace/1`で出力は整形できます。

```elixir
def f() do
  {:current_stacktrace, t} = Process.info(self(), :current_stacktrace)
  # IO.inspect t
  IO.puts Exception.format_stacktrace(Enum.drop(t, 1))
  0
end
```

`Process.info/2`から得た値を[`Enum.drop/2`](https://hexdocs.pm/elixir/Enum.html#drop/2)に渡したのは、`Process.info/2`の呼び出し情報は除きたかったためです。

```terminal
test.exs:3: X.f/0
test.exs:8: X.g/0
test.exs:11: X.h/0
test.exs:15: (file)
(elixir) src/elixir_compiler.erl:79: :elixir_compiler.dispatch/4
(elixir) src/elixir_compiler.erl:63: :elixir_compiler.compile/3
```

[`Exception.format_stacktrace/1`のドキュメント](https://hexdocs.pm/elixir/Exception.html#format_stacktrace/1)は、引数がないときは[`Process.info/2`](https://hexdocs.pm/elixir/Process.html#info/2)から得た現行プロセスのスタックトレース情報を返すように書いています。ただしこの場合、呼び出しもと関数の情報が含まれません。

```elixir
def f() do
  # {:current_stacktrace, t} = Process.info(self(), :current_stacktrace)
  # IO.puts Exception.format_stacktrace(Enum.drop(t, 1))
  IO.puts Exception.format_stacktrace()
  0
end
```

つぎのように、`X.f/0`の情報が抜けてしまいます。

```terminal
test.exs:7: X.g/0
test.exs:10: X.h/0
test.exs:14: (file)
(elixir) src/elixir_compiler.erl:79: :elixir_compiler.dispatch/4
```

[`Exception.format_stacktrace/1`のコード](https://github.com/elixir-lang/elixir/blob/v1.8.0/lib/elixir/lib/exception.ex#L564)を見ると、`Enum.drop/2`で、はじめの3つのトレース情報を削っているのです。

はじめのふたつは`Process.info/2`と`Exception.format_stacktrace/1`ですから、省くことはわかります。けれど、なぜかさらにもうひとつ、呼び出しもと関数(`X.f/0`)の情報も消しているのです。これはあった方がよいという場合は、ダミーのスタック情報をひとつ積んで呼び出せばよいでしょう。

```elixir
def format_stacktrace() do  # スタック情報をひとつ積む
  Exception.format_stacktrace() <> ""
end
def f() do
  # IO.puts Exception.format_stacktrace()
  IO.puts format_stacktrace()
  0
end
```

```terminal
test.exs:6: X.f/0
test.exs:10: X.g/0
test.exs:13: X.h/0
test.exs:17: (file)
```

なお、スタックトレースの情報は7個までしか取得していないようです。ただでさえ少ない情報が3つも削られてしまうのは、かなりもったいないといえます。どのように取得するのがよいかは、少し考えた方がよさそうです。


[^1]: `f() + 1`のように`+1`を加えているのは、末尾呼び出し最適化を妨げるためです。詳しくは「[Elixirのスタックトレースと末尾呼び出し最適化](http://qiita.com/melpon/items/0eedc19d6aece782d13d)」をお読みください。