いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-43 (sentry-goをゴルーチン内で利用する場合の注意点)

概要

Sentry関連で、これもついでにメモ。

インストールとか基本的な使い方については以下を参照ください。

devlights.hatenablog.com

Goではゴルーチンというとても便利な非同期手段があるので

よく非同期処理を手軽に書くことになります。

上記のSentryさんをゴルーチン内で利用する場合、どのゴルーチンの処理かを判別するために

sentry.SetTag("tagname", "tagvalue")

みたいにして、タグつけておきたい場合が多いです。

ですが、非同期処理内で Sentry の スコープ設定をする場合には注意点があって

必ず *sentry.Hub を取得して、Hubに対してスコープ設定やキャプチャをしておく

って言うのがあります。このことは、公式のドキュメントにも記載されています。

docs.sentry.io

上記ページで、何回もスコープという言葉が出てきます。スコープとハブについては以下で記載されています。

docs.sentry.io

てことで、以下に上のページのサンプルをほぼパクリになりますが、だめなパターンと良いパターンについてメモメモ。

駄目なパターン

駄目なパターンというのは、*sentry.Hub を使わずに直接ゴルーチン内でsentry.ConfigureScope()している場合です。

package log_

import (
    "github.com/getsentry/sentry-go"
    "sync"
    "time"
)

// SentryGoroutineBad は、Goroutineの中でSentryを使う場合に「してはいけないパターン」を表しているサンプルです。
// このサンプルのように、Hubを利用せずに直接スコープを定義して処理してはいけない。
func SentryGoroutineBad() error {
    // ----------------------------------------------------------------
    // sentry-go における goroutine 内での利用方法について
    //
    // Goroutine内で利用する場合のやり方について以下のページにコード付きで
    // 記載されている。
    //   https://docs.sentry.io/platforms/go/goroutines/
    // Goroutine内で、スコープを構成する場合は必ず *sentry.Hub を取得して
    // Hubに対して、スコープを構成、および、データのキャプチャを行うようにする。
    //
    // 以下は、わざとHubを利用せずにスコープを構成してメッセージをキャプチャ
    // しているサンプルである。実行すると、メッセージ自体はちゃんと届くが
    // 設定されているタグの値が高確率でおかしくなる。
    //
    // REFERENCES::
    //   https://docs.sentry.io/platforms/go/goroutines/
    //   https://docs.sentry.io/enriching-error-data/scopes/?platform=go
    // ----------------------------------------------------------------
    err := sentry.Init(sentry.ClientOptions{})
    if err != nil {
        return err
    }

    defer sentry.Flush(5 * time.Second)

    wg := sync.WaitGroup{}
    wg.Add(2)

    go func() {
        defer wg.Done()

        // Hubを使わずに直接Goroutine内でスコープを編成してはいけない
        // 他のGoroutineで同じようにスコープを編成していたりするとデータ競合が
        // 発生して設定が上書きされてしまう可能性がある.
        sentry.ConfigureScope(func(s *sentry.Scope) {
            s.SetTag("sentry-goroutine-example-bad", "go#1")
        })

        for i := 0; i < 3; i++ {
            sentry.CaptureMessage("sentry.Hubを使わずにメッセージ送信 from Goroutine#1")
        }
    }()

    go func() {
        defer wg.Done()

        sentry.ConfigureScope(func(s *sentry.Scope) {
            s.SetTag("sentry-goroutine-example-bad", "go#2")
        })

        for i := 0; i < 3; i++ {
            sentry.CaptureMessage("sentry.Hubを使わずにメッセージ送信 from Goroutine#2")
        }
    }()

    wg.Wait()

    // -------------------------------------------
    // このサンプルを実行してSentryに届いた
    // 情報を確認すると、高確率で 2つ目の Goroutine の
    // sentry-goroutine-example-bad タグの値が go#1 となる
    // 本来であれば、 go#2 とならないといけないが
    // Hubを利用せずにスコープを構成しているため
    // データが上書きされてしまっている。
    // -------------------------------------------

    return nil
}

上のサンプルでは、2つのゴルーチンを動かして、一つはタグの値をgo#1にて、もう片方はgo#2にしています。

んで、それぞれのゴルーチンごとにメッセージを3回キャプチャしてSentryにおくっています。

理屈上では、Sentryにてそれぞれのタグ値ごとにフィルタリングすると3つずつになるはずですが

以下のようになります。

まず、"sentry.Hubを使わずにメッセージ送信 from Goroutine#1"ってメッセージの分

f:id:gsf_zero1:20200114013610p:plain
sentry-goroutine-bad-1

これはオッケイですね。メッセージとタグ値がちゃんと一致している。

では、次に"sentry.Hubを使わずにメッセージ送信 from Goroutine#2" ってメッセージの分

f:id:gsf_zero1:20200114013746p:plain
sentry-goroutine-bad-2

紐づくタグの値がおかしいです。go#1になってる。go#2じゃないといけない。

てことで、Hub使わないとやっぱりおかしくなりますね。

良いパターン

以下、ちゃんとゴルーチン内では Hub を利用して設定をしているバージョンです。

package log_

import (
    "github.com/getsentry/sentry-go"
    "sync"
    "time"
)

// SentryGoroutineGood は、Goroutineの中でSentryを使う場合に「こうするべきパターン」を表しているサンプルです。
// このサンプルのように、Hubを利用してスコープを構成しないといけない。
func SentryGoroutineGood() error {
    // ----------------------------------------------------------------
    // sentry-go における goroutine 内での利用方法について
    //
    // Goroutine内で利用する場合のやり方について以下のページにコード付きで
    // 記載されている。
    //   https://docs.sentry.io/platforms/go/goroutines/
    // Goroutine内で、スコープを構成する場合は必ず *sentry.Hub を取得して
    // Hubに対して、スコープを構成、および、データのキャプチャを行うようにする。
    //
    // REFERENCES::
    //   https://docs.sentry.io/platforms/go/goroutines/
    //   https://docs.sentry.io/enriching-error-data/scopes/?platform=go
    // ----------------------------------------------------------------
    err := sentry.Init(sentry.ClientOptions{})
    if err != nil {
        return err
    }

    defer sentry.Flush(5 * time.Second)

    wg := sync.WaitGroup{}
    wg.Add(2)

    // 以下の2つのGoroutineは、それぞれ異なるタイミングでHubを取得しているが
    // どちらも正しい方法となっているため、好きな方を使えば良い。
    // (https://docs.sentry.io/platforms/go/goroutines/ 参照)
    //
    // 大事なのは、Goroutine内では必ずHub経由でSentryにアクセスすること。
    hub := sentry.CurrentHub().Clone()
    go func(h *sentry.Hub) {
        defer wg.Done()

        h.ConfigureScope(func(s *sentry.Scope) {
            s.SetTag("sentry-goroutine-example-good", "go#1")
        })

        for i := 0; i < 3; i++ {
            h.CaptureMessage("sentry.Hubを使ってメッセージ送信 from Goroutine#1")
        }
    }(hub)

    go func() {
        wg.Done()

        h := sentry.CurrentHub().Clone()
        h.ConfigureScope(func(s *sentry.Scope) {
            s.SetTag("sentry-goroutine-example-good", "go#2")
        })

        for i := 0; i < 3; i++ {
            h.CaptureMessage("sentry.Hubを使ってメッセージ送信 from Goroutine#2")
        }
    }()

    wg.Wait()

    // -------------------------------------------
    // このサンプルを実行してSentryに届いた
    // 情報を確認すると、正しくそれぞれのGorouineごとに
    // sentry-goroutine-example-good タグの値が設定されている
    // -------------------------------------------

    return nil
}

まず、"sentry.Hubを使ってメッセージ送信 from Goroutine#1"ってメッセージの分

f:id:gsf_zero1:20200114014106p:plain
sentry-goroutine-good-1

オッケイですね。てことで、もう一つの方。

f:id:gsf_zero1:20200114014206p:plain
sentry-goroutine-good-2

今度はこちらもちゃんと反映されていますね。


過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com