LoginSignup
17
9

More than 3 years have passed since last update.

GoのアプリケーションにDatadogAPMを導入する。

Last updated at Posted at 2019-10-10

業務で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.Starttracer.WithGlobalTag(ext.Environment, datadogEnv)を渡す必要があります。
こうすることで、下記のスクショのように環境の切り替えが行えます。
Screen Shot 2019-10-10 at 14.03.33.png

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!")
})
// 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
}

まとめ

当たり前のことですが、公式のドキュメントだけでなく、ライブラリの実装も見て、自分のアプリケーションに適切な導入方法を見つけ出すことが大切ですね。

17
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
9