エラーの追跡をするシステムはいくつかあるが、WebシステムではSentryやAirbrakeが使われている。ここではAirbrakeの使い方について調べた事を書く。
Airbrakeとは
AirbrakeとはSaaSで、アプリケーションのエラー監視とパフォーマンスモニタリングの機能を提供している。例えばアプリケーションは実行中に発生したエラーをAirbrakeに送信する。Airbrakeは受信したエラーの情報を蓄積し、Webブラウザから確認できるようになる。またエラーを受信したAirbrakeは、エラーの情報をチャットやメールに通知する。そうするとアプリケーションで通知の為の設定を持つ必要がない。
ダミーサーバーを起動する
ダミーサーバーにはerrbitを用いる。またerrbitはMongoDBを使用しているので併せて起動する。以下にwhalebrewの起動例を示す。
MongoDB
,#!/usr/bin/env whalebrew
,image: mongo:4.1
,volumes:
, - "mongo-data:/data/db"
,ports:
, - "27017:27017" # main
, - "27018:27018" # --shardsvr
, - "27019:27019" # --configsvr
,keep_container_user: true
Errbit
,#!/usr/bin/env whalebrew
,image: errbit/errbit:v0.9.0
,ports:
, - "8088:8080"
,entrypoint:
, - "/bin/sh"
, - "-c"
, - "cd /app && bundle exec puma -C config/puma.default.rb"
,environment:
, - "[email protected]"
, - "ERRBIT_ADMIN_PASSWORD=testing1234"
, - "ERRBIT_ADMIN_USER=admin"
, - "MONGO_URL=mongodb://host.docker.internal:27017/errbit"
, - "RACK_ENV=production"
,keep_container_user: true
メールアドレスとパスワードはローカル検証用のため適当なものを入力している。起動したらコンテナにdocker execで入って初期化を実行する。先程設定した環境変数に従って初期アカウントが作成される。
/app # cd /app /app # bundle exec rake errbit:bootstrap Notice: no rspec tasks available in this environment Overwriting existing field _id in class App. Seeding database ------------------------------- Creating an initial admin user: -- email: [email protected] -- password: testing1234 Be sure to note down these credentials now! MONGOID: Created indexes on App: MONGOID: Index: {:name=>"text"}, Options: {:default_language=>"english"} MONGOID: Created indexes on Backtrace: MONGOID: Index: {:fingerprint=>1}, Options: {} MONGOID: Created indexes on Comment: MONGOID: Index: {:user_id=>1}, Options: {} MONGOID: Created indexes on Err: MONGOID: Index: {:problem_id=>1}, Options: {} MONGOID: Index: {:fingerprint=>1}, Options: {} MONGOID: Created indexes on Notice: MONGOID: Index: {:backtrace_id=>1}, Options: {:background=>true} MONGOID: Index: {:created_at=>1}, Options: {} MONGOID: Index: {:err_id=>1, :created_at=>1, :_id=>1}, Options: {} MONGOID: Created indexes on Problem: MONGOID: Index: {:app_id=>1}, Options: {} MONGOID: Index: {:app_name=>1}, Options: {} MONGOID: Index: {:message=>1}, Options: {} MONGOID: Index: {:last_notice_at=>1}, Options: {} MONGOID: Index: {:first_notice_at=>1}, Options: {} MONGOID: Index: {:resolved_at=>1}, Options: {} MONGOID: Index: {:notices_count=>1}, Options: {} MONGOID: Index: {:error_class=>"text", :where=>"text", :message=>"text", :app_name=>"text", :environment=>"text"}, Options: {:default_language=>"english"} MONGOID: Created indexes on User: MONGOID: Index: {:authentication_token=>1}, Options: {}
Airbrakeの設定を環境変数に設定する
ダミーサーバにErrbitを用いているため、Errbitの管理画面のAppsタブを選択しプロジェクトを作成する。
Add a New App
というボタンがあるので、そこから作成できる。
プロジェクトを作成するとプロジェクトIDやプロジェクトキーが作成される。この値を環境変数に設定する。
.env::
AIRBRAKE_ORIGIN=http://127.0.0.1:8088
AIRBRAKE_PROJECT_ID=1
AIRBRAKE_PROJECT_KEY=
AIRBRAKE_ENVIRONMENT=local
Gobrakeを用いた最もシンプルな構成
app_simple.go::
package main
import (
"errors"
"github.com/airbrake/gobrake/v5"
"os"
"strconv"
)
var airbrake *gobrake.Notifier
func main() {
projectId, err := strconv.ParseInt(os.Getenv("AIRBRAKE_PROJECT_ID"), 10, 64)
if err != nil {
panic("No airbrake project id.")
}
airbrake = gobrake.NewNotifierWithOptions(&gobrake.NotifierOptions{
ProjectId: projectId,
ProjectKey: os.Getenv("AIRBRAKE_PROJECT_KEY"),
Environment: os.Getenv("AIRBRAKE_ENVIRONMENT"),
Host: os.Getenv("AIRBRAKE_ORIGIN"),
DisableRemoteConfig: true,
})
defer airbrake.Close()
airbrake.Notify(errors.New("An error occured!!"), nil)
}
Ginで実装したAPIサーバにGobrakeを組み込んでAribrakeにエラーを通知する構成
app_gin_api.go::
package main
import (
"errors"
"github.com/airbrake/gobrake/v5"
ginbrake "github.com/airbrake/gobrake/v5/gin"
"github.com/gin-gonic/gin"
"os"
"strconv"
)
var airbrake *gobrake.Notifier
func main() {
projectId, err := strconv.ParseInt(os.Getenv("AIRBRAKE_PROJECT_ID"), 10, 64)
if err != nil {
panic("No airbrake project id.")
}
airbrake = gobrake.NewNotifierWithOptions(&gobrake.NotifierOptions{
ProjectId: projectId,
ProjectKey: os.Getenv("AIRBRAKE_PROJECT_KEY"),
Environment: os.Getenv("AIRBRAKE_ENVIRONMENT"),
Host: os.Getenv("AIRBRAKE_ORIGIN"),
DisableRemoteConfig: true,
})
defer airbrake.Close()
r := gin.Default()
r.Use(ginbrake.New(airbrake)) // Install middleware for airbrake
r.GET("/ok", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "ok",
})
})
r.GET("/error", func(c *gin.Context) {
airbrake.Notify(errors.New("/error API Error!!"), nil)
c.JSON(500, gin.H{
"message": "Internal Server Error",
})
})
r.Run()
}
zerologにGobrakeを組み込む構成
app_zerolog.go::
package main
import (
"github.com/airbrake/gobrake/v5"
zerobrake "github.com/airbrake/gobrake/v5/zerolog"
"github.com/rs/zerolog"
"io"
"os"
"strconv"
)
var airbrake *gobrake.Notifier
func main() {
projectId, err := strconv.ParseInt(os.Getenv("AIRBRAKE_PROJECT_ID"), 10, 64)
if err != nil {
panic("No airbrake project id.")
}
airbrake = gobrake.NewNotifierWithOptions(&gobrake.NotifierOptions{
ProjectId: projectId,
ProjectKey: os.Getenv("AIRBRAKE_PROJECT_KEY"),
Environment: os.Getenv("AIRBRAKE_ENVIRONMENT"),
Host: os.Getenv("AIRBRAKE_ORIGIN"),
DisableRemoteConfig: true,
})
defer airbrake.Close()
w, err := zerobrake.New(airbrake)
if err != nil {
panic("airbrake was not setup correctly")
}
log := zerolog.New(io.MultiWriter(os.Stdout, w))
log.Error().Msg("Oops!!")
}
この例ではErrorLevelをAirbrakeに通知している。WarnLevelも併せて通知したかったが、gobrakeのzerolog/zerolog.goを確認したところ、以下のように実装されていた。
// Write parses the log data and sends off error notices to airbrake
func (w *WriteCloser) Write(data []byte) (int, error) {
lvl, err := jsonparser.GetUnsafeString(data, zerolog.LevelFieldName)
if err != nil {
return 0, fmt.Errorf("error getting zerolog level: %w", err)
}
if lvl != zerolog.ErrorLevel.String() {
return len(data), nil
}
〜 省略 〜
どうやらWarnLevelを通知するのは一筋縄ではいかないようだった。アプリケーションのログの出力をワーニングからエラーにしたほうがてっとりばやそうだった。
その他
app_panic.go::
package main
func main() {
panic("Oops!!")
}
app_exit_normal.go::
package main
import (
"os"
)
func main() {
os.Exit(0)
}
app_exit_error.go::
package main
import (
"os"
)
func main() {
os.Exit(1)
}
app_tcp_connection_error.go::
package main
func main() {
}
Go Modules
go.mod::
module example
go 1.17
require (
github.com/airbrake/gobrake/v5 v5.5.1
github.com/gin-gonic/gin v1.9.1
github.com/rs/zerolog v1.26.1
)
require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/caio/go-tdigest v3.1.0+incompatible // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
go.sumは長すぎるので省略した。
Foreman
Procfile::
simple: go run app_simple.go
gin: go run app_gin_api.go
zerolog: go run app_zerolog.go