OpenTelemetryパッケージに見るpluginパッケージの利用方法

[Go6アドベントカレンダーの穴埋め投稿](https://qiita.com/advent-calendar/2019/go6)です。

Go Conference 2019 Autumnでどこかの発表で「pluginパッケージは[OpenTelemetry](https://github.com/open-telemetry/opentelemetry-go)で使われていますよ」司会をしたセッションで紹介したのですが、実は最近のコードではその部分は削除されいました。とはいえ、その利用方法というのはpluginパッケージに対応していないWindowsでも使えるような感じで、知っていて損はない感じだったので過去のコミットを掘り出しつつ紹介します。

このエントリーで紹介するテクニックのコードは、以前はルート直下のexporterパッケージの中にありました。そのおかげで目についたのですが、7/12のコードでexperimental/streaming/exporterパッケージに移動になり、最終的に9/24のコミットで削除されました。R.I.P。

* 移動: [Eliminate Scope/ScopeID, separate API from SDK for metrics/stats (#48)](https://github.com/open-telemetry/opentelemetry-go/pull/48)
* 削除: [Remove experimental/streaming globals, add streaming example (#135)](https://github.com/open-telemetry/opentelemetry-go/pull/135)

# プラグインのパッケージ構成

最初のexporter直下にあったときのコードを引っ張り出してきます。OpenTelemetryの情報を[標準出力に出すプラグインstdout](https://github.com/open-telemetry/opentelemetry-go/tree/50f16dd16b82de81420702a9a6f9651ee010ab31/exporter/stdout)です。

```text
- stdout/
  + install/
  | + package.go
  + plugin/
  | + Makefile
  | + package.go
  + stdout.go
```

このうち、stdout.goはプラグインで利用可能になるロジックを含むパッケージです。pluginパッケージなどの外装には影響されない純粋なロジックパッケージです。

## pluginパッケージ

pluginパッケージはpluginのエントリポイントです。

```go:stdout/plugin/package.go
package main

import (
	"github.com/open-telemetry/opentelemetry-go/exporter/observer"
	"github.com/open-telemetry/opentelemetry-go/exporter/stdout"
)

var (
	stdoutObs = stdout.New()
)

func Observer() observer.Observer {
	return stdoutObs
}

func main() {
	_ = Observer()
}
```

```Makefile:Makefile
.PHONY: module

module:
	go build -buildmode=plugin -o stdout.so package.go 
```

## installパッケージ

これが面白いパッケージで、コメントにあるとおりに `import _ "github.com/open-telemetry/opentelemetry-go/exporter/stdout/install"`と利用側のパッケージに書いておくと、静的リンクになります。Windowsのようにpluginパッケージに非対応の環境で使うと良いでしょう。

```go:exporter/stdout/install/pakcage.go
package install

import (
	"github.com/open-telemetry/opentelemetry-go/exporter/observer"
	"github.com/open-telemetry/opentelemetry-go/exporter/stdout"
)

// Use this import:
//
//   import _ "github.com/open-telemetry/opentelemetry-go/exporter/stdout/install"
//
// to include the stderr exporter by default.

func init() {
	observer.RegisterObserver(stdout.New())
}
```

## プラグインの読み込み

`loader`パッケージが同じリポジトリにあります。環境変数で読み込むモジュールを指定していますね。

```go:exporter/loader/loader.go
package loader

import (
	"fmt"
	"os"
	"plugin"
	"time"

	"github.com/open-telemetry/opentelemetry-go/exporter/observer"
)

// TODO add buffer support directly, eliminate stdout

func init() {
	pluginName := os.Getenv("OPENTELEMETRY_LIB")
	if pluginName == "" {
		return
	}
	sharedObj, err := plugin.Open(pluginName)
	if err != nil {
		fmt.Println("Open failed", pluginName, err)
		return
	}

	obsPlugin, err := sharedObj.Lookup("Observer")
	if err != nil {
		fmt.Println("Observer not found", pluginName, err)
		return
	}

	f, ok := obsPlugin.(func() observer.Observer)
	//obs, ok := obsPlugin.(*observer.Observer)
	if !ok {
		fmt.Printf("Observer not valid\n")
		return
	}
	//observer.RegisterObserver(*obs)
	observer.RegisterObserver(f())
}

func Flush() {
	// TODO implement for exporter/{stdout,stderr,buffer}
	time.Sleep(1 * time.Second)
}
```

# まとめ

`plugin`パッケージの使い方の例としてOpenTelemetryのコードを紹介しました。フォールバック手法の用意の仕方が面白いですよね。plugin対応の環境であれば、特定のフォルダをスキャンしてそこの実行形式をプラグインとしてロード、そうでない環境は使いたいpluginを静的ロードしたカスタム版の実行ファイルを作成、みたいな感じの使い分けです。plugin対応の環境であっても、シングルバイナリの方がデバッガーでテストするのも楽でしょうしね。スタックトレースが呼び出し側とplugin内で一括で出てくるようになると思いますし。

とはいえ、バイナリサイズをそこまで気にする必要もないし、pluginも、読み込む側と読み込まれる側で利用するパッケージのバージョンをそろえる必要があったりすることを考えると、用途があまり見当たらないというのが正直なところ。Go Cloudもinstallパッケージによる静的な追加のパターンだけだし、そこまで積極的に使われていないというのは事実かと思います。

OpenTelemetryは、エージェントを置いて、各プログラムはエージェントに送信、エージェントはプラグインで機能拡張、という方向性に見えたんですが、どうも違うみたいですね。実装が落ち着いたらまたじっくりコードを追いかけてみようと思います。