■

一日目

岩木山神社、巖鬼山神社、鶴の舞橋、十三湖、青函トンネル記念館、龍飛崎。

青函トンネル体験坑道:バイオハザード2で通りそう

龍飛崎:ほどほどに雲が晴れて日没を眺める

龍飛崎シーサイドパークで一泊。

一日目

二日目

高野崎、仏ヶ浦、大間崎、恐山。

龍飛崎シーサイドパーク:すごい立地

高野崎:思ってたより過激

高野崎:橋ごとに地質が違う気がする?

むつ湾フェリー:蟹田から脇野沢へ

仏ヶ浦:まさに秘境

恐山:昔ほど雰囲気が無くなった気がするような

うっかり恐山でゆっくりしすぎて、尻屋崎を諦め。東北町で一泊。

二日目

三日目

三沢空港、三沢航空科学館、八食センター、三戸城、九戸城。

三沢航空科学館:展示がよすぎる

三沢航空科学館:飛んだり

三沢航空科学館:回ったり

三戸町:よさそう

無事帰宅。

三日目

三日間で総走行距離は1111km。 とにかく山道が多くて大変だったけど、野生の猿とたくさん出会えて和んだ。

■

ジャズフェスのために仙台に住んでいると言っても過言。

昨日の日中は山形にいたけど、ギリギリ最後だけ覗きに行けた。

二日目は諸事情により青葉山のMPから。 朝の大雨がすぐやんでよかった。 オーガポン・シオカラ節メドレーは何回聞いてもよいね。 俺ファミが裏番組で見に行けなかったのは無念。

仙台駅にワープして、アーケード・勾当台公園・定禅寺通り・西公園と適当に歩きながら、いい感じの演奏に出会って立ち聞きを繰り返し。 今年は例年より人が多い気がする。

リアルずんだパーリナイト、どんなもんかと行ってみたけど、完全に not for me だった。 数曲聞いてみたけど、全然来るものが無くて、早々に退散した。 残念。

この距離感がやっぱ最高。 最後にサックスがめちゃくちゃ上手いバンドが続いてシビれた。

「ジャズフェスが終わる」

「終わるとどうなる?」

「知らんのか」

「オクフェスが始まる」

いやもう始まってたわ、じゃあそういうことで。

■

海行くぞ!

ということで今日はイースターへ。

ではなく南三陸~気仙沼。

荒嶋神社の佇まいがよすぎる。いかにも何か封印されていそう。

あれって道の駅大谷海岸だったんけ。お昼は安くて新鮮な海の幸を堪能。珍しいものもいろいろあったが保冷の備えが無いのでまた今度。

岩井崎って磯遊びもできるんだな。来ねば。

念願のシャークミュージアム、ちょっと狭いけどたまんね~。

そして氷の水族館、マイナス20℃でひと涼み。

マカレル~。カチワって5倍ダメージ与えたくなる病。

浮見堂で時間切れ。今度は唐桑半島まで行きたいね。

■

夏休みなので遠野に河童を捕まえに行ってきた。 小雨決行、8月なのに20℃って.…

まずは丹内山神社へ。 バス停の廃っぷりがいい感じ。

まだ潜れる。

道の駅でひっつみを食ったら五百羅漢へ。 岩と苔と沢が最高。

卯子酉神社と八幡宮にも立ち寄って、いよいよ伝承園。 一千万、遠野ドリーム!

密猟ダメです。

と思ったら昨日までの大雨による増水で今日は禁猟とのこと。 そんな~

北上川も氾濫してるそうだし、さしもの河童も流されてしまったに違いない。

仕方ないので今日のところは見逃してやろう。 かーっ、釣れたわー、今日なら釣れたわー。

そんなわけで時間が余ったので、最後に例の神社。

社は田んぼのド真ん中で近寄れないのでライブカメラには写れない模様。 見てた人によると声は聞こえたらしい。

www.youtube.com

次は絶対捕まえてやるぜ~!

Rails で N+1 をトレースに記録する

N+1 は見つけ出して滅ぼしたくなるのが人情。 とはいえ、時としてそれが発生しているのを見つけるのが難しいことも。

でも Rails では 永らく Bullet ちゃんが助けてくれていますね。

github.com

そんな Bullet のドキュメントに、いつの間にかこんな記述が。

Bullet.opentelemetry = true

ということで、バージョン 8.0.6 以上では N+1 が発生したときに OpenTelemetry の何かが出そうな雰囲気。

Bullet のコードを読んでもそれらしい箇所は無く、通知自体は UniformNotifier というものが担当しているとのこと。 もともと Bullet の一部だったものから切り出したっぽい、へ~。 これはこれで便利そうなので覚えておこう。

github.com

それらしいところを探すと、いかにも lib/uniform_notifier/opentelemetry.rb っぽい。

def _out_of_channel_notify(data)
  message = data.values.compact.join("\n")

  exception = Exception.new(message)
  current_span = OpenTelemetry::Trace.current_span
  current_span.record_exception(exception)
end

なるほどね。 何がどうなるか想像がついたので、実際にやってみるか。

当然お手元の Rails アプリは基本計装済みのはずなので(圧)、Bullet の設定だけでお手軽。 いつも通りに Bullet を bundle exec rails g bullet:install でインストールしても OpenTelemetry の設定は無いので自分で足す。

Bullet.opentelemetry = true

あとは適当にそれっぽくなるコードを埋め込むだけ。

Product.all.each(&:category)

出来上がったトレースを Mackerel で見るとこんな感じ。

予想通り、現在のスパンにエラーとして記録されて便利そう。

フロントエンドが無くてもフロントエンド計装したい

OpenTelemetry でブラウザを計装する方法が公式ドキュメントに書かれていて、その中に次のような記述がある。

opentelemetry.io

<!--
  https://www.w3.org/TR/trace-context/
  Set the `traceparent` in the server's HTML template code. It should be
  dynamically generated server side to have the server's request trace ID,
  a parent span ID that was set on the server's request span, and the trace
  flags to indicate the server's sampling decision
  (01 = sampled, 00 = not sampled).
  '{version}-{traceId}-{spanId}-{sampleDecision}'
-->
<meta
  name="traceparent"
  content="00-ab42124a3c573678d4d8b21ba52df3bf-d21f7bc17caa5aba-01"
/>

ブラウザ計装というと、React や Vue のような SPA だったり、ビルドパイプラインでゴニョゴニョしなきゃならなかったりを想像しがち。 でももっと素朴に、サーバーが HTML を生成して返すとき、その中に現在のトレース文脈を埋め込むことで、そこからブラウザ側の計装へ繋げられる、という話だ。

つまり、狭い意味でのフロントエンドが無くても、広い意味でのフロントエンド計装はできるってわけ。

テンプレートエンジンが接続点

サーバーサイドレンダリングのアプリケーションでは、テンプレートエンジンが HTML を組み立てるとき、サーバー側には「いま処理しているリクエストのトレース文脈」がある。 そこから trace ID、span ID、sampling decision を取り出して meta タグへ埋め込むことで、サーバー側のリクエスト処理とブラウザ側の計測を繋げられるという仕組み。

たとえば Spring Boot と Thymeleaf なら、@ControllerAdvice で共通のモデル属性を追加する形がお手軽。

@ControllerAdvice
class OpenTelemetryTraceAdvice {

    @ModelAttribute("traceparent")
    public String getTraceparent() {
        SpanContext currentContext = Span.current().getSpanContext();

        if (currentContext.isValid()) {
            return String.format("00-%s-%s-%s",
                    currentContext.getTraceId(),
                    currentContext.getSpanId(),
                    currentContext.getTraceFlags().asHex()
            );
        }
        return null;
    }
}

テンプレート側では、値があるときだけ meta タグを出せばいい。

<meta th:if="${traceparent != null}" name="traceparent" th:content="${traceparent}" />

Rails なら before action や view helper あたりでやるとよさそう。

No npm

公式ドキュメントでは、npm でパッケージを入れて Parcel でビルドする流れになってて、それはそうって感じ。 でも、サーバーサイド主体のアプリケーションでお手軽に試したい場合、そこから準備するのはちょっとしんどい。

そこで import map ですよ。

<script type="importmap">
{
  "imports": {
    "@opentelemetry/sdk-trace-web": "https://esm.sh/@opentelemetry/sdk-trace-web",
    "@opentelemetry/instrumentation-document-load": "https://esm.sh/@opentelemetry/instrumentation-document-load",
    "@opentelemetry/context-zone": "https://esm.sh/@opentelemetry/context-zone",
    "@opentelemetry/instrumentation": "https://esm.sh/@opentelemetry/instrumentation",
    "@opentelemetry/resources": "https://esm.sh/@opentelemetry/resources",
    "@opentelemetry/semantic-conventions": "https://esm.sh/@opentelemetry/semantic-conventions",
    "@opentelemetry/exporter-trace-otlp-proto": "https://esm.sh/@opentelemetry/exporter-trace-otlp-proto"
  }
}
</script>
<script type="module" src="/js/document-load.js"></script>

あとは普通に計装して、ブラウザで作られたスパンをサーバーサイドと同じオブザーバビリティバックエンドへ送るだけ。 今回は Mackerel へ。

import { WebTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-web';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';

const exporter = new OTLPTraceExporter({
  url: 'https://otlp-vaxila.mackerelio.com/v1/traces',
  headers: {
    'X-Mackerel-Client-Token': '<MACKEREL_CLIENT_TOKEN>',
  },
});

const provider = new WebTracerProvider({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'frontend',
  }),
  spanProcessors: [new SimpleSpanProcessor(exporter)],
});

provider.register({
  contextManager: new ZoneContextManager(),
});

registerInstrumentations({
  instrumentations: [new DocumentLoadInstrumentation()],
});

もちろん import maps を使う構成が、npm などの代わりにいつでも最適、という話ではない点には注意。 依存関係の固定、最適化、互換性確認、CSP、CDN 障害時の扱いなどを考えると、本格運用ではビルドパイプラインを持つほうが向いている場面も当然ある。

ちなみに、このやり方はサーバーサイド無しの素の HTML 単独でもできる手法なので、覚えておくと何かの時に役に立つかも。

まとめ

そんなこんなで、シンプルな ToDo アプリでのトレースはこんな感じになる。

サーバーサイドのスパンと併せて、ページの取得、DOM の読み込み、ロードイベントなど、ユーザーが画面を開いたときに必ず発生する処理を、同一のトレース内に見られて便利。

「フロントエンドがないからフロントエンド計装は関係ない」と考えるより、「HTML を返しているなら、そこからブラウザ計装へつなげられる」と考えるほうが、サーバーサイドレンダリングのアプリケーションでも観測の解像度が上がってお得。