コンテンツにスキップ

ユーティリティパッケージ@Go

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


air

Goのソースコードに変更があれば、ホットリロードし、コンパイルし直す。


aws-lambda-go

aws-lambda-goとは

以下のリンクを参考にせよ。


aws-sdk-go-v2

aws-sdk-go-v2とは


awsとは

汎用的な関数が同梱されている。

ポインタ型からstring型に変換するToString関数や、反対にstring型からポインタ型に変換するString関数をよく使用する。

▼ serviceパッケージ

記入中...


go-chi

go-chiとは

ミドルウェア処理 (特にルーティング) のパッケージである。

package main

import (
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

func main() {

    r := chi.NewRouter()

    r.Use(middleware.Logger)

    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("welcome"))
    })

    http.ListenAndServe(":3000", r)
}


godocs

godocs

GOROOT変数配下のパッケージのドキュメントを自動的に作成する。

似たものとして、ターミナル上にドキュメントを表示するgo docコマンドがある。


-http

ポート番号を設定する。

デフォルトは6060番である。

$ godoc -http=:8080


gomarkdoc

CI上で実行する

CI上でgomarkdocコマンドを実行する。

差分があれば、CIを失敗させる。

variables:
  GO_VERSION: "1.19.13"

stages:
  - test

go_doc:
  stage: test
  image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/golang:${GO_VERSION}
  script:
    - go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest
    - gomarkdoc ./... --config .gomarkdoc.yml
    - |
      DIFF=$(git diff origin/${CI_COMMIT_BRANCH} --name-only --relative ./)
      echo $DIFF
      if [ -n "$DIFF" ] ; then
        echo "ローカルマシンでgomarkdocを実行し、ドキュメントを更新してください"
        exit 1
      fi


ビルトイン変数

▼ 一覧

{{.Dir}}

ディレクトリ名を再帰的に出力する。

./...を指定すれば、再帰的にドキュメントを作成できる。

$ gomarkdoc ./... -o {{.Dir}}/DOCUMENT.md


--config

設定ファイルを指定して、gomarkdocコマンドを実行する。

# .gomarkdoc.ymlファイル
output: "{{.Dir}}/DOCUMENT.md" # ダブルクオーテーションで囲わないとエラーになる
repository:
  defaultBranch: main
  url: https://github.com/hiroki-hasegawa/foo-repository.git
$ gomarkdoc . --config .gomarkdoc.yml


-e

埋め込みタグの箇所にドキュメントを出力する。

$ gomarkdoc . -o DOCUMENT.md -e
ここに自分のドキュメント

<!-- gomarkdoc:embed:start -->

ここに自動生成のドキュメント

<!-- gomarkdoc:embed:end -->

ここに自分のドキュメント


-o

出力先のファイル名を指定する。

$ gomarkdoc . -o DOCUMENT.md


--repository

ドキュメントにリポジトリ内パッケージのファイルへのリンクを添付する。

$  gomarkdoc . \
     -o DOCUMENT.md \
     --repository.default-branch main \
     --repository.url https://github.com/hiroki-hasegawa/foo-repository.git


go-callvis

go-callvisとは

Goのコールグラフを作成する。

ブラウザ上で確認できる。

main関数のあるファイルパスを指定する必要がある。

# main関数のあるファイル
$ cd app

$ go-callvis .

もしmain関数がないと、エラーになる。

$ go-callvis .

# main関数がない
no main packages


-nostd

Goのビルトインパッケージは除いてグラフ化する。

$ go-callvis -nostd


-nointer

プライベート関数は除いてグラフ化する。

$ go-callvis -nointer


-group

グラフの囲い線を設定する。

デフォルトの囲い線は、pkgである。

$ go-callvis -group pkg,type ./


go-grpc-middleware

gRPCに関するミドルウェア処理 (例:認証、ロギング、メトリクス、分散トレーシングなど) を持つ。

なお、gRPCはリモートプロシージャーコールであるため、ミドルウェア処理にルーティングは含まれない。

v1系とv2系があり、関数の引数の設定方法が異なる。

これをChain関数に渡せば、gRPCで様々なインターセプターを簡単に実行できる。


gorm/plugin/opentelemetry

SQLの発行時に、SQLを属性に持つスパンを自動的に作成する。


grpc-gateway

grpc-gatewayとは

HTTPで受信したリクエストをgRPCに変換してプロキシする。


設定

▼ 独自HTTPヘッダーを保持する

grpc-gatewayでは、デフォルトでは、HTTPヘッダーの独自ヘッダーをgRPCのメタデータに変換せずに破棄してしまう。

特定の条件の時にtrueを返却するmatch関数を定義し、これをruntime.WithIncomingHeaderMatcher関数に渡す。

package main

import (
    "http"
    "log"
    "runtime"
    "strings"
)

func main() {

    ...

    mux := runtime.NewServeMux(
        runtime.WithIncomingHeaderMatcher(matcher),
        runtime.WithForwardResponseOption(filter),
    )

    ...

    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatal(err)
    }
}

func matcher(key string) (string, bool) {
    if strings.HasPrefix(strings.ToLower(key), "x-") {
        return key, true
    }
    return "", false
}


grpc-go

grpc-goとは

GoでgRPCを扱えるようにする。


クライアント側

▼ Dial

DialContext関数のラッパーであり、新しいコンテキストでgRPCサーバーとのコネクションを作成する。

執筆時点 (2024/04/06) でDial関数は非推奨であり、NewClient関数が推奨である。

func Dial(target string, opts ...DialOption) (*ClientConn, error) {
    return DialContext(context.Background(), target, opts...)
}

▼ DialContext

既存コンテキストを使用して、gRPCサーバーとのコネクションを作成する。

執筆時点 (2024/04/06) でDialContext関数は非推奨であり、NewClient関数が推奨である。

package main

import (
    "google.golang.org/grpc"
)

func main() {

    ctx := context.Background()

    ...

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
    )

    ...
}

▼ NewClient

▼ WithBlock

コネクションを確立できるまで待機する。

package main

import (
    "google.golang.org/grpc"
)

func main() {

    ctx := context.Background()

    ...

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
        grpc.WithBlock(),
    )

    ...
}

▼ WithTransportCredentials

gRPCサーバーへの通信をTLS化するかどうかを設定する。

package main

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {

    ctx := context.Background()

    ...

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )

    ...
}


サーバー側

▼ NewServer

既存コンテキストを使用して、gRPCサーバーとのコネクションを作成する。

package main

import (
    "google.golang.org/grpc"
)

func main() {

    ...

    // gRPCサーバーを作成する
    grpcServer := grpc.NewServer()

    ...
}


otel

Tracer

▼ Tracerとは

スパンを作成するためのTracerを作成する。

▼ Start

通常、OpenTelemetryのミドルウェアを実行すると、アプリの最初の関数 (主にmain関数) で自動的にスパンを作成する。

TracerのStart関数を使用すると、これの子スパンを手動で作成することができ、最初の関数の内部でコールされた別の関数の処理時間を計測できるようになる。

package main

import (
    "go.opentelemetry.io/otel"
)

func main()  {

    // ここでOpenTelemetryのミドルウェアを使用すると仮定する
    // main関数のスパンを自動的に作成する

    ...

    // main関数の子スパンとして、foo関数のスパンを手動的に作成する
    foo()

    ...
}

func foo()  {

    // Tracerを作成する
    var tracer = otel.Tracer("計装パッケージ名")

    ctx, span := tracer.Start(
        ctx,
        "foo",
    )

    defer span.End()
}

GetTextMapPropagator

▼ GetTextMapPropagatorとは

設定したPropagatorを取得する。

▼ Extract

リクエストの宛先で、Carrierからトレースコンテキストを抽出する。

package middleware

import (
    "net/http"

    "go.opentelemetry.io/otel/propagation"
)

func fooHandler(w http.ResponseWriter, r *http.Request) {

    ...

    // Carrierのトレースコンテキストを抽出して、既存コンテキストに設定する
    ctx := otel.GetTextMapPropagator().Extract(
        // 抽出したトレースコンテキストの設定先とする既存コンテキストを設定する
        r.Context(),
        // Carrierとして使用するHTTPヘッダーを設定し、トレースコンテキストを抽出する
        propagation.HeaderCarrier(w.Header()),
    )

    ...

}

▼ Inject

リクエストの送信元で、トレースコンテキストをCarrierに注入する。

package middleware

import (
    "net/http"

    "go.opentelemetry.io/otel/propagation"
)

func fooHandler(w http.ResponseWriter, r *http.Request) {

    ...

    otel.GetTextMapPropagator().Inject(
        // トレースコンテキストを持つ既存コンテキストを設定する
        r.Context(),
        // Carrierとして使用するHTTPヘッダーを設定し、トレースコンテキストを注入する
        propagation.HeaderCarrier(w.Header()),
    )

    ...

}


otel/propagation

otel/propagationとは

OpenTelemetryのPropagation


HeaderCarrier

HTTPヘッダーをCarrierとして使用できるようにする。

TextMapCarrierインターフェースの実装である。

type HeaderCarrier http.Header

Header関数をHeaderCarrierに変換することで、HTTPヘッダーをCarrierとして使用する。

package middleware

import (
    "net/http"

    "go.opentelemetry.io/otel/propagation"
)

func fooHandler(w http.ResponseWriter, r *http.Request) {

    ...

    // Carrierのトレースコンテキストを抽出して、既存コンテキストに設定する
    ctx := otel.GetTextMapPropagator().Extract(
        // 抽出したトレースコンテキストの設定先とする既存コンテキストを設定する
        r.Context(),
        // Carrierとして使用するHTTPヘッダーを設定し、トレースコンテキストを抽出する
        propagation.HeaderCarrier(w.Header()),
    )

    ...

}

Ginでも同様にして、HTTPヘッダーをHeaderCarrierに渡す。

package middleware

import (
    "github.com/gin-gonic/gin"

    "go.opentelemetry.io/otel/propagation"
)

func fooHandler(ginCtx *gin.Context) {

    ...

    // Carrierのトレースコンテキストを抽出して、既存コンテキストに設定する
    ctx := otel.GetTextMapPropagator().Extract(
        // 抽出したトレースコンテキストの設定先とする既存コンテキストを設定する
        ginCtx.Request.Context(),
        // Carrierとして使用するHTTPヘッダーを設定し、トレースコンテキストを抽出する
        propagation.HeaderCarrier(ginCtx.Request.Header),
    )

    ...

}


NewCompositeTextMapPropagator

渡された複数のPropagatorからなるComposite Propagatorを作成する。


TextMapPropagator

複数のPropagatorを持つ。

Fields関数でPropagator名を取得できる。

package main

import (
    "go.opentelemetry.io/contrib/propagators/autoprop"
    "go.opentelemetry.io/otel/propagation"
)

func main()  {

    ...

    propagator := autoprop.NewTextMapPropagator()

    // 受信したリクエストのCarrierからトレースコンテキストを抽出し、送信するリクエストのCarrierにトレースコンテキストを注入できるようにする。
    otel.SetTextMapPropagator(
        // Composit Propagatorを設定する
        propagator
    )

    // TextMapPropagatorのFields関数でPropagator名を取得する
    propagatorList := propagator.Fields()

    sort.Strings(propagatorList)

    // ログにpropagator名を出力しておく
    log.Printf("Info: Propagator %v initialize successfully", propagatorList)

    ...
}


otel/sdk

otel/sdkとは

OpenTelemetryのTracerProviderを作成する。


otel/trace

otel/traceとは

記入中...


ContextWithSpanContext

SpanContextを既存コンテキストに設定する。

コンテキストの持つデッドラインやキャンセルは不要で、SpanContextのみを引き継ぐ場合に使える。

package server

import (
    "context"

    "github.com/gin-gonic/gin"
)

func fooHandler(ginCtx *gin.Context) {

    ...

    ctx := trace.ContextWithSpanContext(
        // Ginコンテキストのデッドライン値やキャンセル関数を引き継ぎたくないため、新しくコンテキストを作成する
        context.Background(),
        // GinコンテキストのSpanContextを取得する
        trace.SpanContextFromContext(ginCtx.Request.Context()),
    )

    ...

}


IsRecording

現在の関数のスパンを開始後であれば、trueになる。

現在の関数でスパンを開始していない場合は、falseになる。

package main

import (
    "go.opentelemetry.io/otel"
)

func main()  {

    ...

    foo()

    ...
}

func foo()  {

    // Tracerを作成する
    var tracer = otel.Tracer("計装パッケージ名")

    ctx, span := tracer.Start(
        ctx,
        "foo",
    )

    // スパンを処理中の場合は true になる
    log.Printf("recording span: %v", span.IsRecording())

    defer span.End()
}


RecordError

エラーのイベントを現在のスパンに設定する。

内部的には、AddEvent関数にexceptionイベントを渡している。

IsRecording関数がfalseの場合 (現在の関数でスパンを開始していない場合) は、使用できない。

ただし、イベントの記録はログでやるべきであり、分散トレースとエラーイベントのログを紐付けさえすれば、分散トレース側にエラーイベントの情報を持たせる必要がない。

package server

import (
    "context"
    "net/http"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/codes"
)

func fooHandler(ctx context.Context) {

    ...

    tracer := otel.Tracer("<計装パッケージ名>")

    ctx, span := tracer.Start(
        ctx,
        "foo-service",
    )

    req, err := http.NewRequest(
        "GET",
        "https://example.com",
        nil,
    )

    if err != nil {
        http.Error(w, err.Error(), 500)
        // エラーをスパンに設定する
        span.RecordError(err.Error())
        return
    }

    ...

}


SetStatus

ステータス (UnsetOKError) とエラーメッセージを現在のスパンに設定する。

IsRecording関数がfalseの場合 (現在の関数でスパンを開始していない場合) は、使用できない。

package server

import (
    "context"
    "net/http"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/codes"
)

func fooHandler(ctx context.Context) {

    ...

    tracer := otel.Tracer("<計装パッケージ名>")

    ctx, span := tracer.Start(
        ctx,
        "foo-service",
    )

    req, err := http.NewRequest(
        "GET",
        "https://example.com",
        nil,
    )

    if err != nil {
        http.Error(w, err.Error(), 500)
        // ステータスとエラーをスパンに設定する
        span.SetStatus(codes.Error, err.Error())
        return
    }

    ...

}


SpanContext

トレースコンテキストがもつスパン情報の実体である。

type SpanContext struct {
    traceID    TraceID
    spanID     SpanID
    traceFlags TraceFlags
    traceState TraceState
    remote     bool
}


SpanContextFromContext

既存コンテキストからSpanContextのみを取得する。

現在の関数でスパンを作成していない場合は、上の階層のスパンを取得できる。

package server

import (
    "context"

    "github.com/gin-gonic/gin"
)

func fooHandler(ctx context.Context) {

    ...

    spanCtx := trace.SpanContextFromContext(ctx)

    // トレースIDを確認する
    log.Printf("traceid: %v", spanCtx.TraceID())

    // スパンIDを確認する
    log.Printf("spanid: %v", spanCtx.SpanID())

    // tracestate値を確認する
    log.Printf("tracestate: %v", spanCtx.TraceState())

    ...

}

コンテキストの持つデッドラインやキャンセルは不要で、SpanContextのみを引き継ぐ場合に使える。

package server

import (
    "context"

    "github.com/gin-gonic/gin"
)

func fooHandler(ginCtx *gin.Context) {

    ...

    ctx := trace.ContextWithSpanContext(
        // Ginコンテキストのデッドライン値やキャンセル関数を引き継ぎたくないため、新しくコンテキストを作成する
        context.Background(),
        // GinコンテキストのSpanContextを取得する
        trace.SpanContextFromContext(ginCtx.Request.Context()),
    )

    ...

}


SpanFromContext

スパンIDから既存コンテキストからスパンを取得する。

現在の関数でスパンを作成していない場合は、上の階層のスパンを取得できる。

package server

import (
    "context"

    "github.com/gin-gonic/gin"
)

func fooHandler(ginCtx *gin.Context) {

    ...

    tracer := otel.Tracer("<計装パッケージ名>")

    ctx, _ := tracer.Start(
        ctx,
        "foo-service",
    )

    ...

    span := trace.SpanFromContext(ctx)

    ...

}


Start

新しいスパンIDでスパンを作成する。

package server

import (
    "context"

    "github.com/gin-gonic/gin"
)

func fooHandler(ginCtx *gin.Context) {

    ...

    tracer := otel.Tracer("<計装パッケージ名>")

    ctx, span := tracer.Start(
        ctx,
        "foo-service",
    )

    span.End()

    ...
}


otelgin

otelginとは

受信したリクエストのCarrier (HTTPヘッダー) からGinコンテキスト (gin.Context) を自動的に抽出 (Extract) しつつ、送信するリクエストのCarrier (HTTPヘッダー) にGinコンテキスト (gin.Context) を自動的に注入 (Inject) する。

また、事前のミドルウェア処理としてスパンを自動的に作成する (事後のミドルウェア処理にはotelhttpパッケージを使用する) 。

各関数で事前にスパンを作成する必要がなくなる。

otelginパッケージを使用しない場合、これらを自前で実装する必要がある。


Middleware

リクエスト受信時のミドルウェア処理としてotelginパッケージを設定する。

package main

import (
    "github.com/gin-gonic/gin"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)

func main() {

    ...

    router := gin.New()

    router.Use(otelgin.Middleware("foo-service"))

    router.GET("/foo", fooHandler)

    router.Run(":8080")
}


otelgorm

otelgormとは

クエリ実行前のミドルウェア処理としてスパンを自動的に作成し、事後にはこのスパンにSQLステートメント (gorm.Creategorm.Querygorm.Deletegorm.Updategorm.Rowgorm.Raw) を自動的に設定する。

各永続化関数でスパンを作成したり、SQLステートメントを設定する必要がなくなる。

otelgormパッケージを使用しない場合、これらを自前で実装する必要がある。

func (p *otelPlugin) before(spanName string) gormHookFunc {
    return func(tx *gorm.DB) {
        if tx.DryRun && !p.includeDryRunSpans {
            return
        }
        // 実行中のgormクエリからコンテキストを取得する
        ctx := tx.Statement.Context
        ctx = context.WithValue(ctx, parentCtxKey{}, ctx)
        // スパンを作成する
        // スパン名は、gorm.Create、gorm.Query、gorm.Delete、gorm.Updateなどになる
        ctx, _ = p.tracer.Start(ctx, spanName, trace.WithSpanKind(trace.SpanKindClient))
        tx.Statement.Context = ctx
    }
}

func (p *otelPlugin) after() gormHookFunc {
    return func(tx *gorm.DB) {
        if tx.DryRun && !p.includeDryRunSpans {
            return
        }
        span := trace.SpanFromContext(tx.Statement.Context)
        // スパンを処理していない場合は、処理を終える
        if !span.IsRecording() {
            return
        }
        defer span.End()

        attrs := make([]attribute.KeyValue, 0, len(p.attrs)+4)
        attrs = append(attrs, p.attrs...)

        if sys := dbSystem(tx); sys.Valid() {
            attrs = append(attrs, sys)
        }

        vars := tx.Statement.Vars
        if p.excludeQueryVars {
            vars = make([]interface{}, len(tx.Statement.Vars))

            for i := 0; i < len(vars); i++ {
                vars[i] = "?"
            }
        }

        // SQLステートメントを取得する
        query := tx.Dialector.Explain(tx.Statement.SQL.String(), vars...)

        // SQLステートメントをスパンの属性に設定する
        attrs = append(attrs, semconv.DBStatementKey.String(p.formatQuery(query)))
        if tx.Statement.Table != "" {
            attrs = append(attrs, semconv.DBSQLTableKey.String(tx.Statement.Table))
        }
        if tx.Statement.RowsAffected != -1 {
            attrs = append(attrs, dbRowsAffected.Int64(tx.Statement.RowsAffected))
        }

        span.SetAttributes(attrs...)
        switch tx.Error {
        case nil,
            gorm.ErrRecordNotFound,
            driver.ErrSkip,
            io.EOF,
            sql.ErrNoRows:
        default:
            span.RecordError(tx.Error)
            span.SetStatus(codes.Error, tx.Error.Error())
        }

        switch parentCtx := tx.Statement.Context.Value(parentCtxKey{}).(type) {
        case context.Context:
            tx.Statement.Context = parentCtx
        }
    }
}


NewPlugin

クエリ実行前のミドルウェア処理としてotelginパッケージを設定する。

package db

import (
    "github.com/uptrace/opentelemetry-go-extra/otelgorm"
    "gorm.io/gorm"
)

func NewDb()  {

    db, err := gorm.Open(mysql.Open("<DBのURL>"), &gorm.Config{})

    if err != nil {
        panic(err)
    }

    // ミドルウェアを設定する
    if err := db.Use(otelgorm.NewPlugin()); err != nil {
        panic(err)
    }

    ...
}


otelgrpc

otelgrpcとは

受信したリクエストのCarrier (メタデータ) からコンテキストを自動的に抽出 (Extract) しつつ、送信するリクエストのCarrier (メタデータ) にコンテキストを自動的に注入 (Inject) する。

また、事前/事後のミドルウェア処理としてスパンを自動的に作成する。

各関数で事前/事後にスパンを作成する必要がなくなる。

otelgrpcパッケージを使用しない場合、これらを自前で実装する必要がある。


metadataSupplier

gRPCのメタデータをCarrierとして使用できるようにする。

TextMapCarrierインターフェースの実装である。

プライベートな構造体であり、クライアント側とサーバー側のインターセプター内で使用するようになっている。

type metadataSupplier struct {
    metadata *metadata.MD
}


クライアント側

▼ ClientInterceptor系関数

gRPCリクエスト送信時のインターセプター処理としてotelgrpcパッケージを設定する。

抽出時のメタデータは、mdOutgoingKeyキーとrawMD{md: <メタデータ>}で登録される。

そのため、ユーザー定義のメタデータはmdOutgoingKeyキーで登録できるOutgoingContext系関数で設定する必要がある。

執筆時点 (2024/03/31) でClientInterceptor系関数は非推奨であり、NewClientHandler関数が推奨である。

package main

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters"
    "google.golang.org/grpc"
)

func main() {

    ctx := context.Background()

    ...

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
        // クライアント側のミドルウェア処理としてUnaryClientInterceptorを挿入する
        grpc.WithChainUnaryInterceptor(
            otelgrpc.UnaryClientInterceptor(),
        ),
    )

    ...
}

内部的には、リクエストの送信直前のミドルウェア処理として、注入処理を実行している。

func inject(ctx context.Context, propagators propagation.TextMapPropagator) context.Context {

    // コンテキストにメタデータがあれば取得する
    md, ok := metadata.FromOutgoingContext(ctx)

    // メタデータがなければ作成する
    if !ok {
        md = metadata.MD{}
    }

    propagators.Inject(
        // トレースコンテキストを持つ既存コンテキストを設定する
        ctx,
        // Carrierとして使用するメタデータを設定し、トレースコンテキストを注入する
        &metadataSupplier{
            metadata: &md,
        },
    )

    // メタデータをコンテキストに設定する
    return metadata.NewOutgoingContext(ctx, md)
}

▼ NewClientHandler

執筆時点 (2024/03/31) でClientInterceptor系関数は非推奨になっており、これの移行先である。

package main

import (
    "google.golang.org/grpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)

func main() {

    ...

    // gRPCサーバーとのコネクションを作成する
    conn, err := grpc.DialContext(
        ctx,
        ":7777",
        // クライアント側を一括でセットアップする
        grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
    )

    ...

}


サーバー側

▼ ServerInterceptor系関数

gRPCリクエスト受信時のインターセプター処理としてotelgrpcパッケージを設定する。

抽出時のメタデータはmdIncomingKeyキーとrawMD{md: <メタデータ>}で登録される。

そのため、ユーザー定義のメタデータはmdIncomingKeyキーで登録できるOutgoingContext系関数で設定する必要がある。

執筆時点 (2024/03/31) でServerInterceptor系関数は非推奨であり、NewServerHandler関数が推奨である。

func extract(ctx context.Context, propagators propagation.TextMapPropagator) context.Context {

    // コンテキストにメタデータがあれば取得する
    md, ok := metadata.FromIncomingContext(ctx)

    // メタデータがなければ作成する
    if !ok {
        md = metadata.MD{}
    }

    return propagators.Extract(
        // 抽出したトレースコンテキストの設定先とする既存コンテキストを設定する
        ctx,
        // Carrierとして使用するメタデータを設定する
        &metadataSupplier{
            metadata: &md,
        },
    )
}

▼ NewServerHandler

執筆時点 (2024/03/31) でServerInterceptor系関数は非推奨になっており、これの移行先である。

package main

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters"
    "google.golang.org/grpc"
)

func main()  {

    ...

    // gRPCサーバーを作成する
    grpcServer := grpc.NewServer(
        grpc.StatsHandler(otelgrpc.NewServerHandler(
                otelgrpc.WithFilter(filters.Not(filters.HealthCheck()),
            ),
        ),
    )

    defer grpcServer.Close()
}

▼ WithInterceptorFilter

サーバー側でスパンを作成しないリクエストを設定する。

package main

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters"
    "google.golang.org/grpc"
)

func main()  {

    ...

    // gRPCサーバーを作成する
    grpcServer := grpc.NewServer(
        // 単項RPCのサーバーインターセプターを設定する
        grpc.ChainUnaryInterceptor(
            otelgrpc.UnaryServerInterceptor(
                // ヘルスチェックパスではスパンを作成しない
                otelgrpc.WithInterceptorFilter(filters.Not(filters.HealthCheck())),
            ),
        ),
    )

    defer grpcServer.Close()
}

▼ filters.Not

スパンを作成しない条件を設定する。

package main

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters"
    "google.golang.org/grpc"
)

func main()  {

    ...

    // gRPCサーバーを作成する
    grpcServer := grpc.NewServer(
        // 単項RPCのサーバーインターセプターを設定する
        grpc.ChainUnaryInterceptor(
            otelgrpc.UnaryServerInterceptor(
                // ヘルスチェックパスではスパンを作成しない
                otelgrpc.WithInterceptorFilter(filters.Not(filters.HealthCheck())),
                // 指定したgRPCサービスではスパンを作成しない
                otelgrpc.WithInterceptorFilter(filters.Not(filters.ServiceName("<gRPCサービス名>"))),
                // 指定したgRPC関数ではスパンを作成しない
                otelgrpc.WithInterceptorFilter(filters.Not(filters.MethodName("<gRPC関数名>"))),
            ),
        ),
    )

    defer grpcServer.Close()
}


クライアント/サーバー共通

▼ WithSpanOptions

スパンに付与するオプションを設定する。

package grpc

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "go.opentelemetry.io/otel/attribute"
)

func ChainUnaryServerInterceptor() grpc.UnaryServerInterceptor {

    // 共通のミドルウェア処理としてUnaryServerInterceptorを挿入する
    return otelgrpc.UnaryServerInterceptor(
        otelgrpc.WithSpanOptions(
            // 属性を設定する
            trace.WithAttributes(attribute.String("env", "<実行環境名>")),
        ),
    )
}

▼ WithSpanNameFormatter (オプションなし)

gRPCにこのオプションはない。

gRPCの場合、リモートプロシージャーコールなため、スパン名は関数名とするとよい。


otelhttp

otelhttpとは

受信したリクエストのCarrier (HTTPヘッダー) からコンテキストを自動的に抽出 (Extract) しつつ、送信するリクエストのCarrier (HTTPヘッダー) にコンテキストを自動的に注入 (Inject) する。

また、事前/事後のミドルウェア処理としてスパンを自動的に作成する。

各関数で事前/事後にスパンを作成する必要がなくなる。

otelhttpパッケージを使用しない場合、これらを自前で実装する必要がある。


クライアント側

▼ NewTransport

リクエスト送信時のミドルウェア処理としてotelhttpパッケージを設定する。

package main

import (
    "net/http"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func main() {

    ...

    // HTTPサーバーとのコネクションを作成する
    client := http.Client{
        Transport: otelhttp.NewTransport(http.DefaultTransport)
    }

    ...

}


サーバー側

▼ NewHandler

リクエスト受信時のミドルウェア処理としてotelhttpパッケージを設定する。

package main

import (
    "net/http"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func main() {

    // HttpHandlerを作成する
    fn := func(w http.ResponseWriter, r *http.Request) {
        ...
    }

    // サーバー側のミドルウェア処理としてNewHandlerを挿入する
    otelMiddleware := otelhttp.NewHandler(
        fn,
        // Operation名を設定する
        "foo-service",
    )
}

▼ WithFilter

サーバー側でスパンを作成しないリクエストを設定する。

package main

import (
    "net/http"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func main() {

    // HttpHandlerを作成する
    fn := func(w http.ResponseWriter, r *http.Request) {
        ...
    }

    // サーバー側のミドルウェア処理としてNewHandlerを挿入する
    otelMiddleware := otelhttp.NewHandler(
        fn,
        // Operation名を設定する
        "foo-service",
        otelhttp.WithFilter(filters.All(filters.Not(filters.Path("ヘルスチェックパス")))),
    )
}


クライアント/サーバー共通

▼ WithSpanOptions

スパンに付与する属性を設定する。

package http

import (
    "net/http"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func SetSpanOptions() otelhttp.Option {

    return otelhttp.WithSpanOptions(
        trace.WithAttributes(attribute.String("env", "<実行環境名>")),
    )
}

▼ WithSpanNameFormatter

スパン名を生成する関数を設定する。

HTTPの場合、スパン名はURLにするとよい。

package http

import (
    "fmt"
    "net/http"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func SetSpanNameFormatter(next http.Handler) http.Handler {

    return otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
        // URLパスをスパン名とする
        spanName := r.URL.Path
        if spanName == "" {
            spanName = fmt.Sprintf("HTTP %s route not found", r.Method)
        }
        return spanName
    })
}


otlptracegrpc

otlptracegrpcとは

OTLP形式でテレメトリーを送信するExporterを作成する。

これは、gRPCによるHTTPプロトコルで監視バックエンド (デフォルトではhttps://127.0.0.1:4317) に送信する。

OpenTelemetry Collectorを使用している場合、ReceiverのgRPC用のエンドポイントに合わせる。


otlptracehttp

otlptracehttpとは

OTLP形式でテレメトリーを送信するExporterを作成する。

これは、HTTPプロトコルで監視バックエンド (デフォルトではhttps://127.0.0.1:4318/v1/traces) に送信する。

OpenTelemetry Collectorを使用している場合、ReceiverのHTTP用のエンドポイントに合わせる。


sqlcommenter

sqlcommenterとは

gormで発行したSQLにスパンの情報をコメントアウトとして付与する。

コメントアウトであるため、SQLの動作には影響がない。

import (
    "database/sql"

    gosql "github.com/google/sqlcommenter/go/database/sql"
    sqlcommentercore "github.com/google/sqlcommenter/go/core"
    _ "github.com/lib/pq" // or any other database driver
)

var (
  db *sql.DB
  err error
)

func NewDB(

    ...

    db, err = gosql.Open("<driver>", "<connectionString>",
        // SQLに付与するコメント
        sqlcommentercore.CommenterOptions{
            Config: sqlcommentercore.CommenterConfig{<flag>:bool}
            Tags  : sqlcommentercore.StaticTags{<tag>: string}
        }
    )

    ...

)


sqlmock

▼ New

func NewDbMock(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, error) {

    sqlDB, sqlMock, err := sqlmock.New()

    assert.NilError(t, err)

    // モックDBを作成する
    mockDB, err := gorm.Open(
        mysql.New(mysql.Config{
            Conn:                      sqlDB,
            SkipInitializeWithVersion: true,
        }),
        &gorm.Config{}
    )

    return mockDB, sqlMock, err
}


propagator

TextMapCarrier

Carrierのインターフェースである。

様々な計装ツールのCarrierがこのインターフェースの実装になっている。

otel/propagationパッケージには、HTTPヘッダーをCarrierとして使用するためのTextMapCarrierインターフェースの実装がある。


TextMapPropagator

▼ TextMapPropagatorとは

Propagatorを複数持つ。


propagator/autoprop

propagator/autopropとは


NewTextMapPropagator

ote/propagationパッケージのNewCompositeTextMapPropagatorのラッパーであり、Composite Propagatorを作成する。

デフォルトでは、W3C Trace ContextとBaggageのComposite Propagatorになる。

また、OTEL_PROPAGATORS変数 (tracecontextbaggageb3b3multijaegerxrayottracenone) でPropagator名をリスト形式 (tracecontext,baggage,xray) で指定していれば、上書きできる。

package main

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/propagation"
)

func main()  {

    ...

    // デフォルトでは、W3C Trace ContextとBaggageになる
    otel.SetTextMapPropagator(autoprop.NewTextMapPropagator())

    ...
}
package main

import (
    "go.opentelemetry.io/contrib/propagators/aws/xray"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/propagation"
)

func main()  {

    ...

    // Propagatorを追加する場合は、明示的に指定する
    // 環境変数でも良い
    otel.SetTextMapPropagator(autoprop.NewTextMapPropagator(
        propagation.TraceContext{},
        propagation.Baggage{},
        xray.Propagator{},
    ))

    ...
}


validator

validatorとは


バリデーションとエラーメッセージ

package validators

import (
    "fmt"

    "github.com/go-playground/validator"
)

type FoobarbazValidator struct {
    Foo string `json:"foo" validate:"required"`
    Bar string `json:"bar" validate:"required"`
    Baz string `json:"baz" validate:"required"`
}

// NewValidator コンストラクタ
func NewValidator() *Validator {

    return &Validator{}
}

// Validate バリデーションを実行する
func (v *FoobarbazValidator) Validate() map[string]string {

    err := validator.New().Struct(v)

    var errorMessages = make(map[string]string)

    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            switch err.Field() {
            // フィールドごとにmapでバリデーションメッセージを構成する
            case "foo":
                errorMessages["foo"] = v.stringValidation(err)
                errorMessages["foo"] = v.requiredValidation(err)
            case "bar":
                errorMessages["bar"] = v.stringValidation(err)
            case "baz":
                errorMessages["baz"] = v.stringValidation(err)
                errorMessages["baz"] = v.requiredValidation(err)
            }
        }
    }

    return errorMessages
}

// stringValidation string型指定のメッセージを返却する
func (v *FoobarbazValidator) stringValidation(err validator.FieldError) string {
    return fmt.Sprintf("%s は文字列のみ有効です", err.Field())
}

// requiredValidation 必須メッセージを返却する
func (v *FoobarbazValidator) requiredValidation(err validator.FieldError) string {
    return fmt.Sprintf("%s は必須です", err.Field())
}
package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/foobarbaz-repository/validators"
)

func main() {

    v := NewFoobarbazValidator()

    // JSONを構造体にマッピングする
    err := json.Unmarshal([]byte(`{"foo": "test", "bar": "test", "baz": "test"}`), v)

    if err != nil {
        log.Print(err)
        return
    }

    // バリデーションを実行する
    errorMessages := v.Validate()

    if len(errorMessages) > 0 {
        // mapをJSONに変換する
        byteJson, _ := json.Marshal(errorMessages)
        log.Printf("%v", byteJson)
    }

    // エンコード結果を出力する
    fmt.Println("データに問題はありません。")
}


zap

Logger

▼ Sync

プロセスの終了時に、バッファーに保管されているログを全てフラッシュする。

package main

import (
    "go.uber.org/zap"
)

func main() {

    // Loggerを作成する
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    ...

}

▼ With

以降の構造化ログにキーを追加する。

package main

import (
    "go.uber.org/zap"
)

func main() {

    // Loggerを作成する
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    // キーバリューの対応が明確
    logger := logger.With(
        zap.String("foo", "FOO"),
        zap.String("bar", "BAR"),
    )

    logger.Info("Failed to fetch URL")

    ...

}
package main

import (
    "go.uber.org/zap"
)

func main() {

    // Loggerを作成する
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    // キーバリューの対応が曖昧
    logger := logger.With(
        "foo", "FOO",
        "foo", "BAR",
    )

    logger.Info("Failed to fetch URL")

    ...

}


SugaredLogger

▼ SugaredLoggerとは

ZapのLoggerのラッパーである。

w

構造化ログを作成する。

package main

import (
    "go.uber.org/zap"
)

func main() {

    // Loggerを作成する
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    // LoggerからSugaredLoggerを作成する
    sugar := logger.Sugar()

    sugar.Infow(
        // ログメッセージ
        "This is my first Log with Zap",
        // 構造化データ
        zap.Int("int num", 3),
        zap.Time("Time", time.Now()),
        zap.String("String", "Hello, Zap"),
    )

    sugar.Infof("Failed to fetch URL: %s", url)
}

▼ With

以降の構造化ログにキーを追加する。

package main

import (
    "go.uber.org/zap"
)

func main() {

    // Loggerを作成する
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    // LoggerからSugaredLoggerを作成する
    sugar := logger.Sugar()

    sugar.With(
        // キーバリューの対応が明確
        zap.String("foo", "FOO"),
        zap.String("bar", "BAR"),
    )

    sugar.Infof("Failed to fetch URL: %s", url)
}
package main

import (
    "go.uber.org/zap"
)

func main() {

    // Loggerを作成する
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    // LoggerからSugaredLoggerを作成する
    sugar := logger.Sugar()

    sugar.With(
        // キーバリューの対応が曖昧
        "foo", "FOO",
        "bar", "BAZ",
    )

    sugar.Infof("Failed to fetch URL: %s", url)
}