Intro
最近はGoでApiサーバーを作っています。早いうちからコードをdev環境にデプロイして、みんなでワイワイそれを叩いてフロントを開発しているのですが、いかんせん開発中なのでバグがあります。
そうるるとpodにログを見に行って、500エラーを返している箇所付近のログを探すのですが、そもそも詳細な値をログに落としていなかったり、ログ中のどの行がどのリクエストに対応しているのか分からず、調査は難航します。
そこで、traceを取ってみることにしました。
traceは主に処理時間の内訳や呼び出しの依存関係を示してくれますが、スパンのコンテキストにいろんな値を保持しておき、それをエラーログと結びつけることでデバッグに便利に使えます。また、自身でtraceやlogの記録・可視化を行う環境を構築することは面倒ですが、GrafanaCloudの無料枠を使うとこれが快適に行えるので今回はこれを活用します。
最終的に次のような画面を手に入れることができます。
material: https://gist.github.com/totegamma/1089a9431258bddcc32613dca2715e94
トレース結果をstdoutに出してみる
「ある処理がここから始まり、ここで終わる」というのをコード上で指定してスパンとして記録する行為のことを「計装」と呼びます。
ある程度計装を自動でやってくれる便利なライブラリもあるのですが、一旦手動で計装してみましょう。
また、traceを取得るにあたり、今回はOpenTelemetryのライブラリを使います。
今回使うベースのコードです。
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package main
import (
"os"
"fmt"
"context"
"math/rand"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"go.opentelemetry.io/otel/attribute"
)
var (
// 引数の値が自動的にspanのattributeのotel.library.nameに設定される
// 大体パッケージごとにグローバル変数として定義しておく
tracer = otel . Tracer ( "main" )
)
func main () {
// tracerの初期化
cleanup , err := setupTraceProvider ( "example-service" , "1.0.0" )
if err != nil {
panic ( err )
}
defer cleanup ()
// アプリ全体を囲むspan
ctx , span := tracer . Start ( context . Background (), "app-main" )
defer span . End ()
// 親ctxを引き継いでspanを作ると親子関係になる
randomNum := random ( ctx , 1000 , 10000 )
primeNum := nthPrime ( ctx , randomNum )
fmt . Printf ( "prime number: %d\n" , primeNum )
}
func setupTraceProvider ( serviceName string , serviceVersion string ) ( func (), error ) {
// otelのライブラリではexporterという枠組みで計測した情報をどこに送信するかを設定できる
// 今回はとりあえず標準出力に出してみる
exporter , err := stdouttrace . New (
stdouttrace . WithPrettyPrint (),
stdouttrace . WithWriter ( os . Stderr ),
)
if err != nil {
return nil , err
}
// ここでresourceを設定することで、全てのspanに共通のメタデータを設定できる
resource := resource . NewWithAttributes (
semconv . SchemaURL ,
semconv . ServiceNameKey . String ( serviceName ),
semconv . ServiceVersionKey . String ( serviceVersion ),
)
// 実際に計測するためのtracerProviderを作成する
tracerProvider := sdktrace . NewTracerProvider (
sdktrace . WithBatcher ( exporter ),
sdktrace . WithSampler ( sdktrace . AlwaysSample ()),
sdktrace . WithResource ( resource ),
)
otel . SetTracerProvider ( tracerProvider )
// tracerProviderを終了するための関数を返す
cleanup := func () {
ctx , cancel := context . WithCancel ( context . Background ())
defer cancel ()
tracerProvider . Shutdown ( ctx )
}
return cleanup , nil
}
// generate random number with min max
func random ( ctx context . Context , min , max int ) int {
_ , span := tracer . Start ( ctx , "random" )
defer span . End ()
return rand . Intn ( max - min ) + min
}
// calculate nth prime number
func nthPrime ( ctx context . Context , n int ) int {
_ , span := tracer . Start ( ctx , "nthPrime" )
// 引数をattributeとして記録
span . SetAttributes ( attribute . Int ( "n" , n ))
defer span . End ()
var primes [] int
var i int = 2
for len ( primes ) < n {
var isPrime bool = true
for _ , prime := range primes {
if i % prime == 0 {
isPrime = false
break
}
}
if isPrime {
primes = append ( primes , i )
}
i ++
}
return primes [ len ( primes ) - 1 ]
}
ランダムな数を生成する関数を呼び出し、その結果を元にn番目の素数を求める謎のプログラムです。
実行すると、次の結果がコンソールから出てきます。
consoleout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
{
"Name" : "random" , // スパンの名前
"SpanContext" : {
"TraceID" : "53516838170f0449b41bca826764651e" , // 今回の出力全体で同じTraceIDが入っている
"SpanID" : "2ec3b95a39c60062" , // このスパンだけを指すのID
"TraceFlags" : "01" ,
"TraceState" : "" ,
"Remote" : false
},
"Parent" : {
"TraceID" : "53516838170f0449b41bca826764651e" ,
"SpanID" : "6d99d12c221d2d99" , // 親スパンであるMainのID
"TraceFlags" : "01" ,
"TraceState" : "" ,
"Remote" : false
},
"SpanKind" : 1 ,
"StartTime" : "2023-06-17T14:56:03.896251471+09:00" ,
"EndTime" : "2023-06-17T14:56:03.896259261+09:00" ,
"Attributes" : null ,
"Events" : null ,
"Links" : null ,
"Status" : {
"Code" : "Unset" ,
"Description" : ""
},
"DroppedAttributes" : 0 ,
"DroppedEvents" : 0 ,
"DroppedLinks" : 0 ,
"ChildSpanCount" : 0 ,
"Resource" : [ // tracerProviderを作ったときに指定したパラメーター
{
"Key" : "service.name" ,
"Value" : {
"Type" : "STRING" ,
"Value" : "example-service"
}
},
{
"Key" : "service.version" ,
"Value" : {
"Type" : "STRING" ,
"Value" : "1.0.0"
}
}
],
"InstrumentationLibrary" : {
"Name" : "main" ,
"Version" : "" ,
"SchemaURL" : ""
}
}
{
"Name" : "nthPrime" ,
"SpanContext" : {
"TraceID" : "53516838170f0449b41bca826764651e" ,
"SpanID" : "441b16e84d4e5db0" ,
"TraceFlags" : "01" ,
"TraceState" : "" ,
"Remote" : false
},
"Parent" : {
"TraceID" : "53516838170f0449b41bca826764651e" ,
"SpanID" : "6d99d12c221d2d99" ,
"TraceFlags" : "01" ,
"TraceState" : "" ,
"Remote" : false
},
"SpanKind" : 1 ,
"StartTime" : "2023-06-17T14:56:03.896260631+09:00" ,
"EndTime" : "2023-06-17T14:56:03.969072862+09:00" ,
"Attributes" : [
{
"Key" : "n" , // このときのパラメーターの値が記録されている
"Value" : {
"Type" : "INT64" ,
"Value" : 9853
}
}
],
~省略~
}
{
"Name" : "app-main" ,
"SpanContext" : {
"TraceID" : "53516838170f0449b41bca826764651e" ,
"SpanID" : "6d99d12c221d2d99" ,
"TraceFlags" : "01" ,
"TraceState" : "" ,
"Remote" : false
},
"StartTime" : "2023-06-17T14:56:03.896247231+09:00" ,
"EndTime" : "2023-06-17T14:56:03.969128681+09:00" ,
~省略~
}
それっぽい感じありますね!次は、これをGrafana Tempoでリアルタイムに可視化していきましょう。
Grafana Cloudの設定
今回使うのはGrafanaスタックの中でも、可視化する「Grafana」というソフトウェアと、トレースを保存してくれる「Tempo」というソフトウェアになります。(後でログを保存するために「Loki」も使います)。
これらはオープンソースで提供されており、docker-composeで簡単に環境自体は立ち上げられるのですが、今回はクラウドサービスである「Grafana Cloud」を使ってみます。
GrafanaCloudは結構大きめの無料枠があり、保存期間自体はそんなに長くないものの、環境をまるっと用意してくれて便利です。
アカウントは https://grafana.com/auth/sign-up/create-user から作成できます。
で、これちょっとだけ初見殺しなんですが、アカウント作ってアクセスできるGrafanaのリンクはhttps://<yourstackname>.grafana.com
ですが、こちらは「書き込み済みのデータをクエリしてビジュアライズする」ことがスコープなので「どうやってデータをPrometheusやLokiやTempoに書き込むか」ということはスコープ外です。
GrafanaCloudがホストしてくれているPrometheusやLokiやTempoなどのデータソースに書き込むための情報は、https://grafana.com/orgs/<your-org-name
のコンパネから見れます。 ( https://grafana.com/ にアクセスして右上の"My Account"から辿ると早いです。)
Grafana Agentのセットアップ
GrafanaCloudがホストしてくれている各種データソースへデータを送るに当たり、GrfanaAgentを使うと便利です。公式DockerImageがあるので何もインストールせずに使えます。
まずはConfigを作成します。
agent.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
traces :
configs :
- name : default
receivers :
otlp :
protocols :
http :
endpoint : 0.0.0.0 : 4318
remote_write :
- endpoint : <your endpoint>
basic_auth :
username : <your username>
password : <your apikey>
batch :
timeout : 5s
send_batch_size : 100
書き込むためのエンドポイント、ユーザー名は直前に書いたgrafana.comのコンパネのtempoのDetailsから見ることができます。
APIキーは https://grafana.com/orgs/<your stack name>/api-keys
から作成できます。
configができたら、agent.yamlという名前で保存し、同じディレクトリで
start docker
1
docker run -v $( pwd ) /agent.yaml:/etc/agent/agent.yaml -p4318:4318 grafana/agent
を実行すれば動き始めるはずです。
これで4318ポートでtaraceを待ち受けてくれます。便利!
トレースをagentに送ってGrafanaで確認
では、Goのアプリの方で、トレースの情報を標準出力ではなく先程建てたGrafanaAgentに送るようにしましょう。
と言っても、exporterの部分を書き換えるだけです。
main.go tracerProvider内抜粋
1
2
3
4
5
exporter , err := otlptracehttp . New (
context . Background (),
otlptracehttp . WithEndpoint ( "localhost:4318" ),
otlptracehttp . WithInsecure (),
)
プログラムを実行し、Grafana上のexplorerタブからデータソースとしてデフォルトで設定されているTempoである「grafanacloud-<stackname>-traces」を選択すればトレースが出てくるはずです!
nthPrimeが律速段階になっていることや、またnthPrimeのattributesを見ると、このときのnが9644だったことが分かります。
トレースが出てこない場合はGrafanaAgentのログや、Grafanaの画面上の時計アイコンから範囲がシビアになっていないか等を確認してみてください。
echoでapiサーバー化
最初の例はすぐに終了するプログラムなのでTraaceのありがたみがあんまりありませんでしたね。なので次はこれをechoでAPIサーバー化してみます。
とは言えども、実はOpentelemetryでTraceを自動で出してくれるechoのMiddlewareの実装がすでに公開されているので、これを使うだけです!
https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho
変化のない関数はここでは省略しています。
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main
import (
"context"
"math/rand"
"net/http"
"strconv"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
)
var (
tracer = otel . Tracer ( "main" )
)
func main () {
cleanup , err := setupTraceProvider ( "localhost:4318" , "example-service" , "1.0.0" )
if err != nil {
panic ( err )
}
defer cleanup ()
e := echo . New ()
e . HidePort = true
e . HideBanner = true
e . Use ( middleware . Recover ())
// /metrics と /health は計測しないようにする
skipper := otelecho . WithSkipper (
func ( c echo . Context ) bool {
return c . Path () == "/metrics" || c . Path () == "/health"
},
)
e . Use ( otelecho . Middleware ( "api" , skipper ))
e . GET ( "/randomprimes" , genRandomPrimeNumber )
e . Logger . Fatal ( e . Start ( ":8000" ))
}
func genRandomPrimeNumber ( c echo . Context ) error {
ctx , span := tracer . Start ( c . Request (). Context (), "HandlerGet" )
defer span . End ()
countStr := c . QueryParam ( "count" )
span . SetAttributes ( attribute . String ( "count" , countStr ))
count , err := strconv . Atoi ( countStr )
if err != nil {
span . RecordError ( err )
span . SetStatus ( codes . Error , err . Error ())
return c . JSON ( http . StatusBadRequest , echo . Map {
"error" : "invalid count" ,
})
}
var results [] int
for i := 0 ; i < count ; i ++ {
randomNum := random ( ctx , 1000 , 10000 )
primeNum := nthPrime ( ctx , randomNum )
results = append ( results , primeNum )
}
return c . JSON ( http . StatusOK , echo . Map {
"result" : results ,
})
}
これをcurlで呼び出してみます。
1
2
3
[ totegamma@17:28] ~/simpletrace$ curl "localhost:8000/randomprimes?count=10"
{ "result" :[ 103583,37879,33071,94291,15679,36097,17837,12239,27611,47711]}
[ totegamma@17:28] ~/simpletrace$
無事結果が帰ってきています。Grafana上でも確認してみます。
HTTPリクエストの内容が自動的にAttributeに格納されていますね!これは便利!
また、今回のコードではリクエストからcountをintにパースしていますが、ここで失敗した際にエラーをスパンにも記録させています。
1
2
3
4
5
6
7
8
count , err := strconv . Atoi ( countStr )
if err != nil {
span . RecordError ( err )
span . SetStatus ( codes . Error , err . Error ())
return c . JSON ( http . StatusBadRequest , echo . Map {
"error" : "invalid count" ,
})
}
試しにcountをabcとしてクエリを飛ばしてみます。
1
2
3
[ totegamma@17:32] ~/simpletrace$ curl "localhost:8000/randomprimes?count=abc"
{ "error" :"invalid count" }
[ totegamma@17:32] ~/simpletrace$
すると、Grafana上でもSpanが異常終了しているマークが表示され、エラーの内容も確認できるようになりました。
echoのアクセスログにtraceIDとspanIDを設定する
ところで、echoのアクセスログにそのリクエストを担当したtraceおよびspanを記録しておくと、logとtraceが紐付けられて便利です。
echoのロガーをカスタマイズして、traceIDとspanIDを自動的に記録するようにします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
e . Use ( middleware . LoggerWithConfig ( middleware . LoggerConfig {
Skipper : func ( c echo . Context ) bool {
return c . Path () == "/metrics" || c . Path () == "/health"
},
Format : `{"time":"${time_rfc3339_nano}",${custom},"remote_ip":"${remote_ip}",` +
`"host":"${host}","method":"${method}","uri":"${uri}","status":${status},` +
`"error":"${error}","latency":${latency},"latency_human":"${latency_human}",` +
`"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n" ,
CustomTagFunc : func ( c echo . Context , buf * bytes . Buffer ) ( int , error ) {
span := trace . SpanFromContext ( c . Request (). Context ())
buf . WriteString ( fmt . Sprintf ( "\"%s\":\"%s\"" , "traceID" , span . SpanContext (). TraceID (). String ()))
buf . WriteString ( fmt . Sprintf ( ",\"%s\":\"%s\"" , "spanID" , span . SpanContext (). SpanID (). String ()))
return 0 , nil
},
}))
注意: これは「otelecho middlewareが作成した情報を読んで使っているので、「otelmiddlewareをUseした後でUseする」ようにしてください。
middleware.Loggerの代わりにmiddleware.LoggerWithConfigを使うと様々なカスタマイズができます。
ここでは、ログにもskipperを設定した上で、CustomTagFuncを設定することで独自のログ要素をログに挿入するようにしています。
これで、ログにtraceID, spanIDを記載することができました。
accesslog (見やすいように整形)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"time" : "2023-06-17T17:38:37.506306723+09:00" ,
"traceID" : "9679463138c4c19f2e8e5521bf8add04" ,
"spanID" : "d4c06ddbb47faa72" ,
"remote_ip" : "127.0.0.1" ,
"host" : "localhost:8000" ,
"method" : "GET" ,
"uri" : "/randomprimes?count=abc" ,
"status" : 400 ,
"error" : "" ,
"latency" : 34659 ,
"latency_human" : "34.659µs" ,
"bytes_in" : 0 ,
"bytes_out" : 26
}
logもGrafana Cloudに送信
ではlogもgrafana cloudに送信しましょう!traceはgrafana tempoというデータソースでしたが、logはgrafana lokiというデータソースを使います。
こちらもtempoと同様に設定していきます。
agent.yaml (追記)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
logs :
configs :
- name : default
positions :
filename : /tmp/positions.yaml
scrape_configs :
- job_name : default
static_configs :
- targets : [ localhost]
labels :
job : accesslog
__path__ : /log/access.log
clients :
- url : <your url>
ログの場合は、ログファイルを指定することでそのファイルからログを随時読み込んでくれます。
なので、echoのプログラムの方もログを標準出力ではなくログファイルに落としてあげるようにします。
1
2
3
4
5
logfile , err := os . OpenFile ( "access.log" , os . O_RDWR | os . O_CREATE | os . O_APPEND , 0666 )
if err != nil {
log . Fatal ( err )
}
defer logfile . Close ()
できたら、次はaccesslogをdockerに共有するようにコマンドを修正してagentを起動します。
start docker
1
docker run -v $( pwd ) /access.log:/log/access.log -v $( pwd ) /agent.yaml:/etc/agent/agent.yaml -p4318:4318 grafana/agent
Explorerでデータソースgrafanacloud-<stackname>-traces
を指定して、job=access.log
フィルタを設定すればログが見れるようになると思います。
logからtraceを開く
そして!なんと!logをクリックしてTraceIDの横に現れるボタンを押すだけで、そのログに対応するTraceを参照できます!!!すごい!!!!!!
これは、予めこのTempoデータソースに、ログ上のTraceIDを抽出できた場合、マクロ的にクエリを生成してパネルを開くボタンを作るというのが設定されているからなんですね。
でもってspanIDもやりたい!!!と思われるとおもいますが、これはちょっと難しく…。
というのも、より個数が大きくなるであろうSpanIDの方がTraceIDよりも短いことからお察しだとは思いますが、SpanIDは「そのTrace内で一意」であることを意図していて、「グローバルで一意」であることは意図されていないんですね。
なので、あるSpanを参照したい場合、SpanIDと、そのSpanが所属するTraceIDを一緒に渡して上げる必要があるわけですが、この辺をRegexでゴニョゴニョするのがちょっと難しかったしそこまでやってもあまりユースケースなさそう(Traceで開ければ十分)なので諦めました。
traceからlogを開く
trace画面を色々見ていると、Traceの詳細画面に「Logs for this span」というボタンがあることに気づきますが、グレーアウトされています。
というのも、デフォルトのtempoデータソースの設定だとこの部分が未設定になっているんですよね。
で!!!!!!!!
いっちょ設定書き足してあげるか~となるのですが、デフォルト設定のgrafanacloud-<stackname>-hogehoge は設定が変えられないようになっているので、自分で別にデータソースの設定を作ってあげる必要があります。ナンテコッタイ。
Grafana上のConnectionのData sourcesから「+ Add new data source」を選択してtempoとlokiのデータソースの接続設定を改めて作ります。(僕はなんか空設定のtempoとlokiがすでにあったのでそれの設定を変更しました)
(注: ここでやっているのは「接続設定」なので、データソース本体を新しく作っているわけではないです。データソースの接続設定を改めて作り直しても、接続先のデータソースは変わってないのですでに投入したデータはそのまま参照できます。)
で、大事なのはTempoの設定項目の中の「Trace to logs」ですね。
ここで、“Use custom query"にチェックを入れて、クエリ{job="accesslog"} | json | spanID = "${__span.spanId}"
を設定しておきます。
こうすることで、ボタンを押すと自動でそのボタンのあるspanIDで検索するページに飛べるというカラクリですね。シンプルながら強力な機能です。(ここでは手抜きでjob=accesslogをハードコードしていますが、これはspanから参照できると尚良さそうですね。いい感じにしておいてください。)
これで、traceから関連するログを逆引きできるようになりました!やったー!
今はアクセスログしかありませんが、例えばデバッグ用のログとして{"spanID": "...", "message": "debug info, 123"}
みたいなログを落としていれば、それをすぐに参照できそうですね。
余談ですが、ここでついでにtempoの設定で"node graph"をenableにしておくと、呼び出しの依存関係をグラフで表示してくれるようになります。
NextStep
分散トレーシング
アプリケーション単体だとこうやって愚直にトレースをとるよりももっと簡単で効果的なデバッグ方法がありそうですが、トレースがその魅力を発揮するのはマイクロサービス主体のバックエンドのような、
リクエストが1つのサーバーだけで処理されるのではなく、複数の役割の異なるサーバーを経由して処理されるようなケースらしいですね。(伝聞)(未体験)
このように複数のコンポーネント間を跨いでトレースを取る方法を分散トレーシングと呼ぶらしいです。
技術的には、たとえば今回otelechoMiddleware上で新しくTraceIDを生成しましたが、例えば呼び出し元が別のサービスだった場合、そのサービスですでに生成済みのTraceIDと、このサービスを呼び出したSpanIDをParentSpanIDとして受け取り(これらの情報をTraceContextと呼ぶらしい)それをもとに処理を継続することで実現可能です。
マイクロサービスをやる機会があったら、ぜひやってみようと思います。
おまけ 計装ライブラリ
いろいろと簡単に計装するためのライブラリがあるお
for gorm
https://github.com/go-gorm/opentelemetry
実際に発行されたクエリまで見れてすごい!便利!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 接続した後にtracing.NewPlugin()をUseするだけ
db , err := gorm . Open ( postgres . Open ( config . Server . Dsn ), & gorm . Config {})
if err != nil {
panic ( "failed to connect database" )
}
err = db . Use ( tracing . NewPlugin ())
if err != nil {
panic ( "failed to setup tracing plugin" )
}
// 使うときはwithContextしないと記録されない(それはそうんだんだけど罠)
var host core . Host
err := r . db . WithContext ( ctx ). First ( & host , "id = ?" , key ). Error
return host , err
for go-redis
https://pkg.go.dev/github.com/go-redis/redis/extra/redisotel
実際に発行されたクエリまで見れてすごい!便利2!
設定はgormとあんまり変わらない
1
2
3
4
5
6
7
8
9
rdb := redis . NewClient ( & redis . Options {
Addr : config . Server . RedisAddr ,
Password : "" , // no password set
DB : 0 , // use default DB
})
err = redisotel . InstrumentTracing ( rdb )
if err != nil {
panic ( "failed to setup tracing plugin" )
}
redisの場合クエリの実行にcontextの指定が必須なので罠がない。これだけでOK。ラクチン!!!!!