テック技報最新号へ →
コード

Go: gospan で span を SQLite に溜め、「どこが遅いか」を SQL で探す

collector もエージェントも立てずに、1 プロセス内の処理時間を span として SQLite ファイルに書き出すライブラリ。pure Go なので CGO も要らない。開発中や不具合調査で「どの子処理が遅いか」を後から SQL で調べたいときに使う。

@matsuu · 投稿 2026-10-18
#go#observability#tracing#sqlite#net-http

どんな時に使う?

  • ログに経過時間は出ているが、どの DB 呼び出しや外部呼び出しが遅いのか分からない
  • OpenTelemetry と収集基盤を入れるほどの規模ではない
  • 調査用に、実行ごとのトレースをファイルで残しておきたい

やり方

go
import (
    "github.com/akmadian/gospan"
    "github.com/akmadian/gospan/sqlite"
)

sink, err := sqlite.New("traces", sqlite.WithName("run.sqlite", true)) // 既定は実行ごとに別ファイル
if err != nil { log.Fatal(err) }
tracer, err := gospan.New(sink)
if err != nil { log.Fatal(err) }
gospan.SetDefault(tracer) // これを呼ぶまで gospan.Start は何もしない
defer tracer.Close(context.Background())

// ミドルウェア: span 名は低カーディナリティに、パスや ID は属性へ
ctx, span := gospan.Start(r.Context(), "http.request",
    slog.String("http.route", route), slog.String("http.path", r.URL.Path))
defer span.End()

// 子処理(ctx を渡すと親子になる)
ctx, q := gospan.Start(ctx, "db.query", slog.String("db.table", "users"))
defer q.End()
q.SetAttrs(slog.Int("rows", n)) // 途中で分かった値を後から追加
// 失敗時は q.Fail(err)

defer gospan.Track(ctx, "cache.lookup")() // 1 行で書ける葉 span(子は作れない)

実行後は spans_named ビュー(名前と duration_ns を結合済み)や attrs テーブルを SQL で引く。

sql
SELECT name, ROUND(duration_ns / 1e6, 1) AS ms, status
FROM spans_named ORDER BY duration_ns DESC LIMIT 10;

slog に流す gospan.SlogSink(logger) もあり、終わった span が 1 行のログになる。

注意点

  • tracer.Stats().Dropped を確認する。既定ではバッファ(8192 件)が溢れると、イベントを捨てて件数だけ数える。取りこぼしたくないなら WithBlockingPolicy() を使う(その代わり呼び出し側が待たされる)。
  • プロセスをまたぐ context の伝播はない。サービス間の分散トレースが必要なら OpenTelemetry を使う。
  • End しなかった span は書き出されない。強制終了すると、最後の Flush(1 秒ごと)以降の分は失われる。
  • v0.1 時点の pre-1.0。ファイル形式は固定だが、Go の API は変わる可能性がある。go.mod は go 1.25 以上が必要。
  • nil の tracer や span のメソッドは何もしないので、計装はコードに残したまま、環境変数で tracer を作るかどうかだけ切り替えられる。

出典

X の元の投稿を開く