Advent of Code 2025 を完走した

毎年12月に開催されている Advent of Code に、2019年から参加している。

過去記事:

昨年の10周年を終え、今年からは例年の半分・12日間の開催となった2025年。

adventofcode.com

どうにかすべての問題に解答して 24 個のスターを集めることができた。

よ、ようやく終わった…! / I just completed all 12 days of Advent of Code 2025! #AdventOfCode adventofcode.com

[image or embed]

— すぎゃーん (@sugyan.com) December 21, 2025 at 2:00 PM

今年も色々な問題があり楽しかった。半分の量だとだいぶ精神的にラク…!やはり例年の25日間というのはハードだったんだなと思う。 あとはやっぱりリアルタイムに同じ問題に取り組む仲間がいるととても嬉しい!

今年はAdvent of Codeに同じように挑戦(Zigで!)している仲間が社内にいるので、すぐに回答を共有したり感想を言い合えたりしてとても心強いし楽しい…!

— すぎゃーん (@sugyan.com) December 10, 2025 at 2:54 PM

生成AIなどには頼らず全部自力で解こうと努力したが、Day 10 のpart2だけは数日間試行錯誤し続けたがどうにもならず…悔しいがChatGPTにヒントを出してもらってようやく解が出せた。2つアプローチ試してダメで諦めてしまったが その2つを組み合わせれば解けていたかぁ…くやしい

最後のDay 12はまったくよく分からなかったり 自分の中で消化しきれていないものもあるので、Redditなどで他の回答みたり生成AIと相談したりしつつ復習したり、OCamlなど他の言語でも解いてみたいと思います。

Repository

Anybatross YAPC::Fukuoka 2025 で2位タイだった #yapcjapan #anybatross

perlbatross.kayac.com

techblog.kayac.com

前回までの参加記録はこちらで書いていました

今回は総合 -198 で、kurainさん masiuchiさんと並んでの2位タイでした。

提出解

自分の最終的な提出解はこちら

Hole 1: Counter Counter

print$s+=$\=y/8B/0/+y/0469ADO-R//.$/,","for<>

Score: -55 (45 bytes)

Hole 2: BPEagle

s=gets;?A.upto(?Z){(k,),v=s.scan(/(?=(\S\S))/).tally.max_by{_2};v>1&&(s.gsub!k,it;$*<<it+?:+k)};puts$**?,,s

Score: -143 (107 bytes)

遅れての参加

以前はYAPCの開催期間に合わせての開催で、YAPC終了後数日、という期間だったと思うけど、今回は開催期間が長く、YAPCよりも早く開始して 終了後も10日間ほどある、というものだった。

自分はYAPC::Fukuokaで登壇の機会をいただいており その準備で精一杯で、このAnybatrossをやり始めてしまうと絶対に発表準備が終わらなくなる…!と危惧していたので、YAPC::Fukuoka 2日目が終了するまで絶対にAnybatrossを開かないようにしていた。

なので、自分が取り組み始めた11/16には既にほぼ最短解たちが出揃っている状態になっていて、他にも多くの参加者が解を提出していた。

Perlbatrossのときから、「自分のスコアより低いものは他の人の回答を見ることが出来る」という仕様があるため、今回はそれを最大限に利用させてもらった。 最初はできるだけ自分で考えつつできるだけ短くしていって、行き詰まったら近いスコアの他の回答を覗いてヒントを得て それを利用してスコアを伸ばし、また良いスコアの回答を覗けるようになったらそこからアイデアをパクらせてもらい… の繰り返し。 そのおかげで比較的短い時間で今回のスコアまで到達することができた。自分一人の力では全然このスコアには到達しなかったと思う。

感想

Hole 1

shebangが効くかな?と思ったけど結局効果なしで普通に for<> が強かった。

なんかすごい奇抜なテクニックで数えられるかな?と考えたけど結局正規表現でマッチさせるくらいしかなさそうだった。ただ 8B を他の文字に寄せて再度数える、というのはなるほど…と感心した。 y/8B//*2+y/0469ADO-R// より1byte短くなる。

変数を2つ使って改行をそのまま利用する

print$s+=$c=y/8B/0/+y/0469ADO-R//,",$c
"for<>

というのもできたけど長さは変わらず。それなら1行で収まる方が自分は好きかな。 出力の区切りが , じゃなくて だったらこれでもう1byte削れたのになぁ…

print$s+=$\=y/8B/0/+y/0469ADO-R//.$/,$"for<>

カウントしても累積和を先に出力しないといけない、とか絶妙にイヤらしい仕様の問題だった。

Hole 2

最初に問題読んだときは「こんな複雑な問題をゴルフするなんて…」と絶句した。が、やってみると予想外に面白かった。

Perlでは全然無理そうだ…と早々に諦めてRubyで。Rubyは初心者なので tally とかあるの初めて知りましたわ…。shebangperlと同様に -p できるってのも最後まで知らなかった。知ってても結局短くはできなかったと思うけど。

scan(/(?=(\S\S))/) した結果が配列の配列になってしまうのがイヤらしくてしばらく苦戦したが、 (k,),v= のように代入する、というアイデアはChatGPTに相談しまくってようやく発見できた。

あとはそれ以上短くする方法はどうにも思い浮かばず。考えても考えても、というか 考えようにもRubyの言語仕様や標準ライブラリのAPIなどの知識が無さすぎてアイデアの元がまったく無くてどうしようもない感じ。やはりある程度の知識を持ってGolf力を鍛えている言語でないと戦えないんだな、と痛感した。 in とかまったく思いつかなかったし知らなかったし。

生成AI

結論からいうとほぼ役に立たない。この分野に関してはまだまだ人類の知恵が勝利できると言えるだろう。数年後にはこれも凌駕されるんだろうか?

問題を丸投げするだけでもなく、

  • 自分の回答を見せて、もっと短縮する方法あるか訊く
  • 問題を分解し、実現したい処理を列挙して短く書くテクニックやアイデアを出してもらう

など色々な使い方で手伝ってもらおうとしてみたが、大抵は「既にあなたの回答は最短です」としか答えなかったり そもそも正しく動かないとか仕様を満たさないような頓珍漢な回答を出してきたりばっかりだった。

おわりに

今回もとても楽しく、勉強になりました。ありがとうございました!

次は1位を目指せるよう頑張りたい…!PerlだけじゃなくてRubyも勉強しないとな…

GoでGracefulに停止する定期実行ワーカー

起動してから、指定された処理を「s秒ごと」のように固定間隔で実行し続ける、というワーカー的なプログラムを考える。

何も考慮しない場合

最も原始的には以下のように time.Ticker を使ってループし続ければ良い。

type Worker struct {
    interval time.Duration
    task     func(context.Context)
}

func NewWorker(interval time.Duration, task func(context.Context)) *Worker {
    return &Worker{
        interval: interval,
        task:     task,
    }
}

func (w *Worker) Start(ctx context.Context) {
    ticker := time.NewTicker(w.interval)
    defer ticker.Stop()

    for {
        <-ticker.C
        w.task(ctx)
    }
}

tickerが指定した周期でchannelに値を送信し受信可能状態になるので、それを待ってから次のtaskを実行すれば良い。

処理中のtaskは完了してから終了したい [失敗例]

ここから、例えばシグナルを受け取って終了するが 実行中のものが中断されないよう完了を待ってから停止したい、という場合。

うっかりこう書いてしまいそうだが、これは意図した挙動にならないことがある。

func (w *Worker) Start(ctx context.Context) {
    ticker := time.NewTicker(w.interval)
    defer ticker.Stop()

    for {
        select {
        // ここでcontextのキャンセルを監視
        case <-ctx.Done():
            return
        case <-ticker.C:
            w.task(ctx)
        }
    }
}

func doTask(ctx context.Context) {
    n := 500 + rand.Intn(500)
    log.Printf("Starting task (%d ms)...", n)
    time.Sleep(time.Duration(n) * time.Millisecond)
    log.Printf("Task completed.")
}


func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    NewWorker(time.Second, doTask).Start(ctx)
}

少し長い doTask が実行中にCtrl+Cでストップしようとしても、ちゃんと確実に Task completed. が出てから終了するのを確認できる。

しかし この doTask の実行時間がもっと長く、interval よりも長い場合、次のループに来たときに <- ctx.Done() を受信できるが、同じく <- ticker.C も受信できる状態になっている。この場合 select がどちらを通るかは不定のため、 ctx は既にキャンセルされているのに再度 task が実行される、ということが起こり得る。つまり「止まるかもしれないし 止まらないかもしれない」。

こういう考慮不足のものは意外と最近の生成AIでも書いてきたりするので注意が必要。

処理中のtaskは完了してから終了したい [修正版]

というわけで停止したい場合に確実に停止してもらうために、 ticker.C が選ばれた場合も ctx がキャンセルされているかどうかを必ずチェックするようにする。

func (w *Worker) Start(ctx context.Context) error {
    ticker := time.NewTicker(w.interval)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
            if ctx.Err() != nil {
                return ctx.Err()
            }
            w.task(ctx)
        }
    }
}

もしくは select の分岐内をシンプルに中止か待機かだけの役割にして

   for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
        }
        if ctx.Err() != nil {
            return ctx.Err()
        }
        w.task(ctx)
    }

とか。これなら select をループの後方に持っていくことで最初の1回目の実行を即座に開始できる。

   for {
        if ctx.Err() != nil {
            return ctx.Err()
        }
        w.task(ctx)

        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
    }
}

完了待機時間に上限を設ける

実行中の task が終了するまで待機するようにはなったが、もし何らかの不具合や非常に重い処理をしていたりして長い時間がかかっていると、いつまでもプログラムが終了しない問題がある。

それでは困るので、実行中の task の完了を待つ時間に上限を設定する。

type Worker struct {
    interval        time.Duration
    shutdownTimeout time.Duration
    task            func(context.Context)
}

func NewWorker(interval time.Duration, shutdownTimeout time.Duration, task func(context.Context)) *Worker {
    return &Worker{
        interval:        interval,
        shutdownTimeout: shutdownTimeout,
        task:            task,
    }
}

func (w *Worker) Start(ctx context.Context) error {
    ticker := time.NewTicker(w.interval)
    defer ticker.Stop()

    for {
        if ctx.Err() != nil {
            return ctx.Err()
        }

        // task用のcontextを作成
        taskCtx, taskCancel := context.WithCancel(context.Background())
        done := make(chan struct{})

        go func() {
            defer taskCancel()
            w.task(taskCtx)
            close(done)
        }()

        // taskの完了または親contextのキャンセルを待つ
        select {
        case <-done:
            // task完了
        case <-ctx.Done():
            // shutdownTimeout の間だけtaskの完了を待つ
            // 完了しなければ強制キャンセルして終了
            select {
            case <-done:
            case <-time.After(w.shutdownTimeout):
                taskCancel()
            }
            return ctx.Err()
        }

        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
        }
    }
}

task の実行をgoroutineに載せ、完了を待っている間に ctx.Done() を受信したらさらに完了を待ちつつ shutdownTimeout の時間だけ待機。そちらが先に来てしまったら taskCancel を呼んでから終了する。

task には親ctxをそのまま渡しても良いが、停止リクエスト受信時には即座に停止しようとはせずに実行中のものは出来るだけ完了まで継続して欲しい、という設計も有り得る。そういった場合にはこのように別のcontextを渡し、本当に待機上限時間を超えたときだけキャンセル、という形にできる。 ただし、この場合はcancelを呼んだ後にすぐにプログラムが終了すると本来のcancel処理が実行されずにプロセスごと終了してしまうことが有り得るので、最低限の「ドレイン待ち」のような処理は必要かもしれない。

YAPC::Fukuoka 2025 に参加、登壇してきた #yapcjapan

yapcjapan.org

参加してきました。昨年の函館は参加できなかったので、その前の広島以来の参加。

登壇

今回はついにCfPが採択され、登壇することができました。(広島、函館と応募はしていたけど落選だったので 3度目の正直!) YAPCは2009あたりから何度も参加しているけど、登壇できたのは2010年が最後だったようで、実に15年ぶり(!)のYAPC登壇でした。

2日目の最後の方だったのでなかなか気が抜けなくて厳しかった…

すぎゃーん見に来た! #yapcjapan

[image or embed]

— Yoshiori (@yoshiori.bsky.social) November 15, 2025 at 2:16 PM

発表資料はこちらです。

speakerdeck.com

内容としては趣味プログラミングということで、とにかく自分が楽しい・面白いと思ったものや取り組んだ内容について好き勝手に喋らせてもらいました。 主にパズルを題材に、色んなアプローチを紹介しつつ こういう色んなコードが書けるしプログラミングって面白いですよね、というものです。 できるだけの情報は詰め込んだので、是非資料だけでも見てみてください。

残念ながら人の入りは少なく、8割くらいは知人が聴きに来てくださった(ありがとうございました…!)、という感じでしたが、終わったあとに皆様から「面白かった」と感想をいただけて本当に嬉しかったです。

デモで手間取ったり時間配分をミスって最後の方が駆け足になってしまったりはしたけど、自分なりに好きなことを好きなだけ喋ることができて、それがおそらく聴きに来てくださった皆様にも伝わったと思うので、自分の中ではベストトークができたと思います。大満足!


とはいえ人を集められなかったのは悔しいし、次はもっと多くの人の興味を引けるような良いトークが出来るようになりたいな、とも思ったのでまた次回に向けて精進していきたいと思います。

参加

聴きに行ったトークや企画もどれも面白くて、まったく飽きない2日間でした。面白そうなのがかぶっていて片方しか聴けなかった、とかもたくさんあったので、後で資料や動画(公開されたら)などでじっくり味わいたいと思います。

懇親会では喉が死んでしまっていて満足に会話ができなかったけど、久しぶりの方々や初めましての方にも挨拶できて良かった…! 普段ほぼ喋らないのに急にいっぱい会話をしたせいで喉が潰れたのかな…。

感謝

福岡工業大学という会場もキレイなキャンパスで気持ち良く過ごせるステキな会場でしたね。ありがとうございました!

そして今回も素晴らしいYAPCを作り上げてくださった運営スタッフの皆様、ありがとうございました!

最後に、子どもたちを引き受けて3日間もの遠征に送り出してくれた妻に心から感謝!!おかげさまで目一杯YAPCを楽しむことができました。お土産いっぱい持って帰るね…!

続き

まだ終わりではなく、自分の発表準備で精一杯で手付かずだった Anybatross もこれから取り組んでみようと思います。(これ始めてしまうと自分の準備が絶対終わらんくなると思って強い意思で開かないようにしていた)

React Three Fiberで作る3Dインターネットくす玉

「Kyoto.なんか #7」に参加してきた。

Kyoto.なんか #7 - connpass

そこで id:nagayama さんがReact Three Fiberの話をしていて、Reactの宣言的な書き方でThree.jsの3Dシーンを描画できるの良いなと思った。

r3f.docs.pmnd.rs

以前にThree.jsを使ってみたときにちょっと書いたけどなかなか書き方難しいな、と感じていたのだった…。

memo.sugyan.com

そして会の終了後に id:Windymelt さんがインターネットくす玉をReact Three Fiberで作ろうとしていたのが面白そうだった (懇親会中には完成しなかった)。

blog.3qe.us

ので、自分でも書いて練習しようと思って作ってみた。 Vibe Codingでは手直しが多くなりそうだったので主にCopilotと相談しながら自分の手で。しかしやっぱり型エラーの解消が難しかったな…。

玉をクリックすると割れます。もう1回割りたかったらリロード。 紙吹雪の量とか動きとかは調整してみたかったけど 一旦ここまで。

github.com

オンラインミーティングが始まったら自動で点灯するオンエアーネオンライト macOS版

yoshiori.hatenablog.com

この記事を以前に読んでいてずっと記憶に残っていた。最近になって子どもたちが夏休みに入り家にいる時間が増えたこともあり、必要に迫られて実際に真似してみることにした。

MTG中に妻や子どもたちが部屋に乱入する事故が起こるので yoshioriさんの真似してみることにした。まずは設置まで

[image or embed]

— すぎゃーん (@sugyan.com) July 25, 2025 at 11:48 AM

成果物

github.com

macOSでのカメラ起動検出

macOSではLinuxとは異なり/dev/以下にカメラデバイスファイルが表示されないため、デバイスファイルの使用状況を監視する方法での検出はできない。また、macOSではVDC Assistant(Video Device Control Assistant)プロセスがカメラデバイスへのアクセスを管理し、フレームワークベースのアクセス方式を採用している。そのため、デバイス排他制御や占有状態を基にした検出方法も適用できない。

GPT 4oやClaudeといった生成AIとディスカッションしながら試行錯誤した結果、 CoreMediaIO Framework の kCMIODevicePropertyDeviceIsRunningSomewhere プロパティを利用することで、システム内のどこかでカメラデバイスが使用されているかを検出できることが判明した。このプロパティはBoolean値を返し、カメラが何らかのアプリケーションで使用中かどうかを示す。ただしAppleの公式ドキュメントは存在するものの、実際の実装例は限られている。

実際、生成AIもこれらのAPIを使ったコードを書くのは難しかったようで、最初Swiftで書かせてみたが上手くいかず、Objective-CでPoCを作ってからそれをSwiftに変換してもらう順序で、どうにか動作するものができた。

カメラの起動・停止をイベントとして受け取る仕組みがあればベストだったが、現時点で見つけられた判定方法がこれだけだったため、このAPIを利用して1秒ごとに「何らかのアプリケーションで使用中か」をチェックし、変更があれば通知などの処理を行う方式にした。

SwitchBotへの同期

SwitchBotの設置とハブ経由でのAPI利用は既に 自宅環境監視への入門 - すぎゃーんメモ で経験していたため、スマートプラグを追加購入するだけで簡単に実現できた。

カメラの状態変化を検知してAPIを叩き、オン・オフを同期する処理もClaude Codeに任せたところ、サクッと実装してくれた。結局Swiftコードは1行も自分では書いていない。

自動バックグラウンド起動設定

systemdのような自動起動の仕組みをmacOSでどう実現するか全くわからなかったが、これもClaude Codeに任せたところ~/LaunchAgents/以下に置く.plistファイルのテンプレートを用意してくれ、install scriptを実行するだけで設定が完了した。

生成AIのチカラ

今回は未知の技術が多かったため、自分で調べながら実装していたら、こんな短期間では作れなかったし、クオリティもずっと低いものになっていたと思う。 もちろん何度も議論やレビュー、ダメ出しを繰り返しながらの実装ではあったが、想像以上のものを駆足で作ってもらうことができ、非常に良い体験だった。

Claude Codeのログとメトリクスをさくらのクラウド「モニタリングスイート」に送る

Claude Codeには OpenTelemetry (OTel) のサポートがある。

これを使ってログやメトリクスを収集し、さくらのクラウドの「モニタリングスイート」に送信する試みをした。

Claude CodeのOpenTelemetry設定

ほぼすべてのことがここに書いてある。

docs.anthropic.com

最も簡単には、以下の設定を .claude/settings.local.json などに書けば確認できる。

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "console",
    "OTEL_METRICS_EXPORTER": "console"
  }
}

通常の入出力の他に様々な情報がターミナルに現れるようになる。

otel-collectorで受ける

console出力ではなく、外部システムへの送信を目的としてOpenTelemetry Collectorにデータを流すようにしてみる。

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc"
  }
}

これにより、ログ・メトリクスそれぞれが OTEL_EXPORTER_OTLP_ENDPOINT (デフォルトで http://localhost:4317) に対し grpc で送信されることになる。

これを受け取るOpenTelemetry collectorには、後述する「モニタリングスイート」への中継を前提として以下のものを使用する。

github.com

必要最低限の機能は十分に揃っているはず。ビルド済みバイナリをダウンロードするなりdockerで起動するなり自前でビルドするなりして、repositoryに含まれている config.yaml を使って起動するだけでまずはotel-collectorの出力としてデバッグログが流れるのを確認できる。

./sacloud-otel-collector --config=./config.yaml

verbositydetailed にするとより詳細な内容を見ることができる。

exporters:
  debug:
    verbosity: detailed

ログ:

2025-06-30T12:04:04.885+0900    info    ResourceLog #0
Resource SchemaURL:
Resource attributes:
     -> service.name: Str(claude-code)
     -> service.version: Str(1.0.35)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope com.anthropic.claude_code.events 1.0.35
LogRecord #0
ObservedTimestamp: 2025-06-30 03:04:03.135 +0000 UTC
Timestamp: 2025-06-30 03:04:03.135 +0000 UTC
SeverityText:
SeverityNumber: Unspecified(0)
Body: Str(claude_code.user_prompt)
Attributes:
     -> user.id: Str(***)
     -> session.id: Str(c1acf7b7-d1b2-497b-9512-d8b2728259c4)
     -> organization.id: Str(***)
     -> user.email: Str(***@gmail.com)
     -> user.account_uuid: Str(***)
     -> terminal.type: Str(tmux)
     -> event.name: Str(user_prompt)
     -> event.timestamp: Str(2025-06-30T03:04:03.135Z)
     -> prompt_length: Str(5)
     -> prompt: Str(<REDACTED>)
Trace ID:
Span ID:
Flags: 0
        {"otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs"}

メトリクス:

2025-06-30T12:01:56.754+0900    info    ResourceMetrics #0
Resource SchemaURL:
Resource attributes:
     -> service.name: Str(claude-code)
     -> service.version: Str(1.0.35)
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope com.anthropic.claude_code 1.0.35
Metric #0
Descriptor:
     -> Name: claude_code.session.count
     -> Description: Count of CLI sessions started
     -> Unit:
     -> DataType: Sum
     -> IsMonotonic: true
     -> AggregationTemporality: Delta
NumberDataPoints #0
Data point attributes:
     -> user.id: Str(***)
     -> session.id: Str(5d1084d1-f6f1-4feb-b5fc-4081104e9d6e)
     -> organization.id: Str(***)
     -> user.email: Str(***@gmail.com)
     -> user.account_uuid: Str(***)
     -> terminal.type: Str(tmux)
StartTimestamp: 2025-06-30 03:01:46.884 +0000 UTC
Timestamp: 2025-06-30 03:01:56.662 +0000 UTC
Value: 1.000000
        {"otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics"}

ここで注目すべきところは

  • ログは Bodyclaude_code.user_prompt といったevent名だけが入り、その他の情報は Attributes に含まれる
    • promptの内容は <REDACTED> になっている
  • メトリクスは AggregationTemporality: Delta で送られる

というところ。

otel-collectorからモニタリングスイートへ送る

ここからモニタリングスイートの話。「モニタリングスイート」のスイートは「スイートプリキュア♪」のスイートと同じスイートです。

manual.sakura.ad.jp

上記マニュアルにある通り、さくらのクラウド「モニタリングスイート」では、少なくとも2025年6月現在では対応プロトコル

  • メトリクス: Prometheus Remote Write
  • ログ: OpenTelemetry Protocol (OTLP/HTTP)

となっている。 上述のotel-collectorからそれぞれ対応したexporterを設定することで、それぞれ送信できる。

コントロールパネル上でそれぞれ「ログストレージ」「メトリクスストレージ」を作成する。

各ストレージで対応するアクセスキーを作成。

これらで得たエンドポイントとBasic認証ヘッダを使って、以下のような感じで設定していく。 メトリクスに関しては自分で usernamepasswordからbase64エンコードしてヘッダの値を作る必要がある…。

# 例: username: `m123456`, password: `12345678-1234-5678-9abc-123456789abc`
$ echo -n "m123456:12345678-1234-5678-9abc-123456789abc" | base64
bTEyMzQ1NjoxMjM0NTY3OC0xMjM0LTU2NzgtOWFiYy0xMjM0NTY3ODlhYmM=

exportersを追加した config.yaml は以下のようになる:

exporters:
  debug:
    verbosity: detailed

  prometheusremotewrite:
    endpoint: https://************.metrics.monitoring.global.api.sacloud.jp/prometheus/api/v1/write
    headers:
      Authorization: Basic **********************************************************==

  otlphttp:
    endpoint: https://************.logs.monitoring.global.api.sacloud.jp
    headers:
      Authorization: Basic ********************************************************

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug, prometheusremotewrite]

    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug, otlphttp]

メトリクスの設定

上記の基本設定だけでは、メトリクスは正常に送信されない。DEBUGログを出力するようにすると以下のようなエラー文言が出る。

service:
  telemetry:
    logs:
      level: DEBUG
        {"otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics"}
2025-06-30T23:38:39.874+0900    debug   prometheusremotewriteexporter@v0.125.0/exporter.go:219  failed to translate metrics, exporting remaining metrics        {"otelcol.component.id": "prometheusremotewrite", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics", "error": "invalid temporality and type combination for metric \"claude_code.session.count\"", "translated": 1}

先述の通り、Claude Codeからは AggregationTemporality: Delta で送信されるが、これは prometheusremotewrite exporterでは扱えない、とのこと。

これに対応するための一つの方法として、 deltatocumulative processorが使用できる。

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024
  
  deltatocumulative: {}

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch, deltatocumulative]
      exporters: [debug, prometheusremotewrite]

こうすることで、otel-collector側で累計値を保持して処理してcumulativeの値で書き込んでくれるようになる。

または、Claude Code側で以下の環境変数を設定しておけば、processorは使わなくてもClaude Code側で毎sessionでの累積値を送信してくれるようにはなる。

{
  "env": {
    "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "cumulative"
  }
}

ログの設定

ログは特別な設定なしでも一応送信はできるが、そのままではイベント名しか記録されず、情報量が極めて乏しい。

debug 出力で見た通り、多くの情報は Attributes の中に含まれていて、モニタリングスイートにはこれらの情報を Body に載せる必要がありそう。

ここでは transform processorを使う。

processors:
  transform/add_attrs_into_body:
    log_statements:
      - 'set(log.body, {"message": log.body})'
      - merge_maps(log.body, log.attributes, "upsert")
      - set(log.attributes, {})

service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch, transform/add_attrs_into_body]
      exporters: [otlphttp]

log.bodyMap として持つようにし、元々入っていた Str のBodyは "message" というフィールドで持つよう変更する。そしてすべての log.attributes をその log.body にmergeする。元々の log.attributes は含めていても無駄になるだけなので空にしてしまう。

こうしてやることで、 json_payload として Attributes に含まれていた情報をモニタリングスイートで閲覧することができるようになる。

user_promptprompt はデフォルトで <REDACTED> と記録されるが、これはClaude Code側の OTEL_LOG_USER_PROMPTS の設定で変えることはできる。

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_LOG_USER_PROMPTS": 1
  }
}

さらにHooksも

Claude Codeの最近の更新(v1.0.38以降)では、各イベント時にカスタム処理を実行できるHooks機能が追加されている。

これらをログとして取得して送信したい場合は以下のようなスクリプトを用意しておくと良さそう。

#!/bin/bash

HOOK_NAME="$1"
OTEL_ENDPOINT="${OTEL_ENDPOINT:-http://localhost:4318/v1/logs}"

if [ -z "$HOOK_NAME" ]; then
  echo "Usage: $0 <hook_name>" >&2
  exit 1
fi

jq -n --argjson body "$(cat)" --arg hook "$HOOK_NAME" '{
  resourceLogs: [{
    resource: {attributes: [{key: "service.name", value: {stringValue: "log-sender"}}]},
    scopeLogs: [{
      logRecords: [{
        timeUnixNano: (now * 1000000000 | tostring),
        body: {kvlistValue: {values: ([$body | to_entries[] | {key: .key, value: {stringValue: (.value | tostring)}}] + [{key: "hook.event", value: {stringValue: $hook}}])}},
        attributes: []
      }]
    }]
  }]
}' | curl -X POST \
  -H "Content-Type: application/json" \
  -d @- \
  "$OTEL_ENDPOINT"

otel-collector では receivershttp も指定してあればこのように curl でPOSTすることもできる。 あとは、Claude Code側で hooks 設定をそれぞれ指定する。

{
  "env": {  ...  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [ { "type": "command", "command": "./otel-log-sender.sh PreToolUse" } ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [ { "type": "command", "command": "./otel-log-sender.sh PostToolUse" } ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [ { "type": "command", "command": "./otel-log-sender.sh Notification" } ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [ { "type": "command", "command": "./otel-log-sender.sh Stop" } ]
      }
    ]
  }
}

こうして様々な情報をログとして残しておくことができるようになる。

メトリクス可視化

メトリクスが正常に保存されていれば、モニタリングスイートの可視化機能を使ってダッシュボードを作成できる。

クエリ例:

sum(claude_code_token_usage_tokens_total) by (type, model)

これらの値を使ってアラートを設定して通知送信する、といったこともできるがここでは割愛…

まとめ

Claude CodeのOpenTelemetryサポートを活用して、ログとメトリクスをさくらの「モニタリングスイート」に送信する環境を構築した。

主なポイント:

  • Claude Code側でOTelの設定を行い、otel-collectorに送信
  • メトリクスはDelta形式で送られるため、deltatocumulative processorまたは累積モード設定が必要
  • ログはAttributesの情報をBodyに含めるためtransform processorで変換が必要
  • Hooksを活用することで独自のイベントログも収集可能

これにより、Claude Codeの使用状況を詳細に監視・分析できる環境が整った。メトリクスの可視化やアラート設定により、使用パターンの把握やトークン消費量の管理も行える。