業務でDatadogAPMを導入する機会があったので、詰まった箇所などを簡単にまとめようと思います。
Datadogでやりたいこと
- カスタムエージェントホストの設定
- サービス名の指定
- 環境毎にTraceListのページを切り替える
- 1つのリクエストに、クエリ等のログを紐づける
DatadogTracerのインストール
Tracing Go Applications #Installation
このページに記載されている通りに、下記のコマンドでインストールします。
go get gopkg.in/DataDog/dd-trace-go.v1/ddtrace
現時点では、Frameworkはecho
、ORMはgorm
を使用しているので、今回対象となるファイルは下記です。
$ cd $GOPATH/pkg/mod/gopkg.in/\!data\!dog/dd-trace-go.v1@v1.18.0
$ tree contrib/jinzhu
contrib/jinzhu
└── gorm
├── example_test.go
├── gorm.go
├── gorm_test.go
└── option.go
$ tree contrib/labstack
contrib/labstack
└── echo
├── echotrace.go
├── echotrace_test.go
├── example_test.go
└── option.go
公式のドキュメントを見てみる
参考にしたのは下記の4つ
Tracing Go Applications #Configuration
Tracing Go ApplicationsChange Agent Hostname
GoDoc dd-trace-go.v1: gopkg.in/DataDog/dd-trace-go.v1/contrib/labstack/echo
GoDoc dd-trace-go.v1: gopkg.in/DataDog/dd-trace-go.v1/contrib/jinzhu/gorm
実装
カスタムエージェントホストの設定、サービス名の指定
func startDatadogTrace() {
// See: https://docs.datadoghq.com/ja/tracing/setup/go/#change-agent-hostname
addr := net.JoinHostPort(
os.Getenv("DD_AGENT_HOST"),
os.Getenv("DD_TRACE_AGENT_PORT"),
)
// start the tracer with zero or more options
tracer.Start(tracer.WithServiceName("my-app"), tracer.WithAgentAddr(addr))
}
これでTrace開始の実装完了です。
※呼び出し等で、tracer.Stop()
を忘れずに実装してください。
環境毎にTraceListのページを切り替える
上記の実装だけだと環境毎にTraceListを分けられないので、少し手を加えます。
func startDatadogTrace() {
// See: https://docs.datadoghq.com/ja/tracing/setup/go/#change-agent-hostname
addr := net.JoinHostPort(
os.Getenv("DD_AGENT_HOST"),
os.Getenv("DD_TRACE_AGENT_PORT"),
)
var opts []tracer.StartOption
opts = append(opts, tracer.WithServiceName("my-app"), tracer.WithAgentAddr(addr))
datadogEnv := os.Getenv("DATADOG_ENV")
if datadogEnv != "" {
opts = append(opts, tracer.WithGlobalTag(ext.Environment, datadogEnv))
}
// start the tracer with zero or more options
tracer.Start(opts...)
}
環境変数である必要はありませんが、tracer.Start
にtracer.WithGlobalTag(ext.Environment, datadogEnv)
を渡す必要があります。
こうすることで、下記のスクショのように環境の切り替えが行えます。
1つのリクエストに、クエリ等のログを紐づける
これに結構苦戦しました。
公式のドキュメントをみると、下記のように実装するように書いてあります。
// Create a new instance of echo
r := echo.New()
// Use the tracer middleware with your desired service name.
r.Use(Middleware(WithServiceName("image-encoder")))
// Set up some endpoints.
r.GET("/image/encode", func(c echo.Context) error {
// create a child span to track an operation
span, _ := tracer.StartSpanFromContext(c.Request().Context(), "image.encode")
// encode an image ...
// finish the child span
span.Finish()
return c.String(200, "ok!")
})
- gorm
Example Open
// Register augments the provided driver with tracing, enabling it to be loaded by gormtrace.Open.
sqltrace.Register("postgres", &pq.Driver{}, sqltrace.WithServiceName("my-service"))
// Open the registered driver, allowing all uses of the returned *gorm.DB to be traced.
db, err := gormtrace.Open("postgres", "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable")
defer db.Close()
if err != nil {
log.Fatal(err)
}
user := struct {
gorm.Model
Name string
}{}
// All calls through gorm.DB are now traced.
db.Where("name = ?", "jinzhu").First(&user)
これをほぼそのまま実装してみると、1リクエストにクエリが紐づいて表示されるのですが、それとは別にクエリのみのログも転送されてしまいます。
恥ずかしながら、これを解決するのに結構時間がかかってしまいました。(問い合わせるも、明確な回答は得られず。。。)
そこでDatadogTracerのgromの実装を見てみました。
// Open opens a new (traced) database connection. The used dialect must be formerly registered
// using (gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql).Register.
func Open(dialect, source string, opts ...Option) (*gorm.DB, error) {
sqldb, err := sqltraced.Open(dialect, source)
if err != nil {
return nil, err
}
db, err := gorm.Open(dialect, sqldb)
if err != nil {
return db, err
}
return WithCallbacks(db, opts...), err
}
うん、sqltrace.Register
を事前に実行する必要があるようですね。
となるとやはり公式の実装が正しいと思えてしまいます。
ですが、大事なのはgormtrace.WithCallbacks
関数でした。
// WithCallbacks registers callbacks to the gorm.DB for tracing.
// It should be called once, after opening the db.
// The callbacks are triggered by Create, Update, Delete,
// Query and RowQuery operations.
func WithCallbacks(db *gorm.DB, opts ...Option) *gorm.DB {
afterFunc := func(operationName string) func(*gorm.Scope) {
return func(scope *gorm.Scope) {
after(scope, operationName)
}
}
cb := db.Callback()
cb.Create().Before("gorm:before_create").Register("dd-trace-go:before_create", before)
cb.Create().After("gorm:after_create").Register("dd-trace-go:after_create", afterFunc("gorm.create"))
cb.Update().Before("gorm:before_update").Register("dd-trace-go:before_update", before)
cb.Update().After("gorm:after_update").Register("dd-trace-go:after_update", afterFunc("gorm.update"))
cb.Delete().Before("gorm:before_delete").Register("dd-trace-go:before_delete", before)
cb.Delete().After("gorm:after_delete").Register("dd-trace-go:after_delete", afterFunc("gorm.delete"))
cb.Query().Before("gorm:query").Register("dd-trace-go:before_query", before)
cb.Query().After("gorm:after_query").Register("dd-trace-go:after_query", afterFunc("gorm.query"))
cb.RowQuery().Before("gorm:row_query").Register("dd-trace-go:before_row_query", before)
cb.RowQuery().After("gorm:row_query").Register("dd-trace-go:after_row_query", afterFunc("gorm.row_query"))
cfg := new(config)
defaults(cfg)
for _, fn := range opts {
fn(cfg)
}
return db.Set(gormConfigKey, cfg)
}
結論、これさえ実行させれば、sqltrace.Register
も、gormtrace.Open
も必要ありませんでした。
最終的なgorm周りの実装は下記の通りです。
func connectDB() *gorm.DB {
connection := os.Getenv("MYSQL_CONNECTION")
db, err := gorm.Open("mysql", connection)
if err != nil {
panic(err.Error())
}
// Add callbacks for Datadog.
db = gormtrace.WithCallbacks(db)
return db
}
まとめ
当たり前のことですが、公式のドキュメントだけでなく、ライブラリの実装も見て、自分のアプリケーションに適切な導入方法を見つけ出すことが大切ですね。