« ^ »

Goのプログラム内部のエラーをAirbrakeに送信する

所要時間: 約 4分

エラーの追跡をするシステムはいくつかあるが、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タブを選択しプロジェクトを作成する。

https://res.cloudinary.com/symdon/image/upload/v1653009305/blog.symdon.info/1652919941/errbit-step1_bxu9ex.png

Add a New App というボタンがあるので、そこから作成できる。

https://res.cloudinary.com/symdon/image/upload/v1653009306/blog.symdon.info/1652919941/errbit-step2_clrr2n.png

プロジェクトを作成するとプロジェクト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