Nuphy Air60 のキーキャップを交換した

ちょっと前に格ゲー用のレバーレスコントローラのキースイッチとキャップを交換してからというもの、どうにもキーボード周辺の情報が気になってしまっている。しばらく SNS や YouTube を眺めてこれ欲しいなーなどと夢想していたのだけど、コントローラと同じようにまずはキーキャップを変えて気分も変えてみようと思い立った。

普段使っているキーボードは Nuphy Air60 というやつ。これに合うキーキャップを AliExpress で探してみると、かわいいやつを発見した。ちょうど在庫処分のセールもしていて送料込みで 1,800 円で買うことができた。安すぎる。

かわいい

Windows 用のキーキャップしか入ってなかったのでいくつかのキーはサイズが合うやつで代替した。左 Shift だけ合うやつがなかった元のキーのままでちょっと不格好だけど満足。

Apple Accountで購入したアイテムのアカウント間での移行に失敗したメモ

iPhone や Mac にメインでサインインしているアカウントと、メディアと購入で使っているアカウントをうまい具合に統合しようとしたけど失敗した記録。

まず、Apple Account の状態を整理するとこういう感じ。

Apple One 加入以前

  • メインアカウント
    • iPhone や Mac でサインインしている
    • iCloud+ の 50GB プランに入っている
    • 探すアプリや Apple Store での購入履歴が紐づいてる
  • サブアカウント
    • メディアと購入、サブスクリプションでサインインしている
    • iTunes で買った曲とか App Store で買ったアプリ・サブスクリプションが紐づいている

普通に使ってると知る場面が少なそうだけど、メディアと購入などには別のアカウントでサインインできる。ずっと気持ち悪いなと思っていたけど、実害はあまりないのでこの状態で使い続けていた。ちなみに、なんでこの状態になったのかは全然覚えていない。

ここから Apple Music を使いたくなったので以下の状態に。

Apple One 加入後

  • メインアカウント
    • iPhone や Mac、メディアと購入、サブスクリプションでサインインしている
      • つまり全ての場面でメインアカウントを使っている
      • この運用にした後はこっちのアカウントで曲やアプリを購入
    • Apple One に加入している(iCloud+ 50GB とか Apple Music が含まれる)
    • 探すアプリや Apple Store での購入履歴が紐づいてる
  • サブアカウント
    • 過去に iTunes で買った曲とか App Store で買ったアプリ・サブスクリプションが紐づいている
  • ファミリー共有
    • メインアカウントが管理者、サブアカウントがメンバー
    • 購入アイテムやサブスクリプションを共有しているのでサブアカウントで過去に買ったものがメインアカウントでも使える

iCloud+ を使ってる状態で Apple One に入ると iCloud+ が含まれる状態でアップグレードできるのだけど、それは同じアカウントの場合だけ。なんとも気持ち悪いけど、ファミリー共有でお茶を濁していた。

購入したものを別の Apple Account に移行できるようになった

長年待ち望んでいた機能が来た!!!!本当はアカウントマージをずっと待ってたけど!!実際はこれで十分なので早速試した。

こうしたかった

  • メインアカウント
    • iPhone や Mac、メディアと購入、サブスクリプションでサインインしている
      • つまり全ての場面でメインアカウントを使っている
      • サブアカウントで購入したものが全て紐づいている
    • Apple One に加入している(iCloud+ 50GB とか Apple Music が含まれる)
    • 探すアプリや Apple Store での購入履歴が紐づいてる
  • サブアカウント
    • 使わない
  • ファミリー共有
    • 使わない

現実

移行ボタンが全然出てこないな〜と思いながら移行前に済ませておくべきことを確認してまわっていたら、なんとサブアカウントに電話番号が登録されていない。こういう時のために二回線持ってるし!と思いサブ回線の電話番号を登録しようと思ったらその番号はすでに使われていると出た。

1Password に登録してある Apple Account を全部見て回ってみたけどこの番号使ってないやん〜と思いつつ、ここで引き下がるわけにはいかねぇという勢いのもと povo で新規回線を契約。途中ミスって iPad に eSIM を入れてしまったり、眠っていた Pixel 4a を引っ張り出してきたりして、ようやくサブアカウントに電話番号を登録できた。ついに「購入したアイテムを移行」ボタンが出た!

購入したアイテムを移行

細かく色々条件が書いてあったけどうまくいくかな〜どうかな〜と思いつつボタンを押すと無常にもこの画面に。

購入したアイテムを移行できません。

ドキュメントには「購入したアイテムを移行できない場合」という項目もあったので再度確認してみたけど、いまいちどれに該当するかわからない。せめてどっちのアカウントの問題か教えて欲しい。なんだよ「一方または両方のアカウント」って。

意味が取りづらいのだけどこれだろうか。両方のアカウントで曲を買ってたらダメということ?

日本語: プライマリApple AccountとセカンダリApple Accountの両方で、ミュージックライブラリのデータがそれぞれのアカウントに紐付けられている場合、購入したアイテムは移行できません。

英語: You can’t migrate purchases if both the primary Apple Account and the secondary Apple Account have music library data associated with each of them.

ただ

日本語: プライマリアカウントを購入や無料ダウンロードに使ったことがない場合、購入したアイテムは移行できません。

英語: If your primary account has never been used for purchases or free downloads, you can’t migrate purchases.

とも書いてあるので難しい。プライマリの方でミュージックライブラリの紐付けを外したらいいのか?

もうちょっと試してから「Apple One 加入後」の運用に戻そうと思うけど、今日は元気が売り切れたのでまた後日。あと、勢いで作った povo の回線どうすんのこれ。

Rushbox Lite のキースイッチを Haute42 SHADOW HUNTING に交換した

格ゲー用のコントローラとして Rushbox Lite というやつを使っている。自作キーボード界隈の会社がレバーレスコントローラのクラウドファンディングを始めるということを知り開始直後に支援してゲットしたもの。程よいサイズ感や軽さ、マットな質感など気に入っていて、 大きな不満はなかったのだけど、もう少しキーストロークが浅ければなと思っていた。

2024年の年末ごろに Haute42 が SHADOW HUNTING という押下圧も軽くアクチュエーションポイントやストロークも短いキースイッチを出したというのを知り、試しに交換してみることにした。最初はアリエクで買おうかと思ったけど日本の Amazon でも大して値段が変わらなかったので Amazon で購入。

キーボードも含め、キースイッチ周りをいじるのは初めてだったけど、Rushbox にキースイッチプラーが付属していて、公式のマニュアルでも動画付きでやり方が解説されているので特に迷わず交換できた。

ボタンのレイアウトを変更する |Rushboxマニュアル

SHADOW HUNTING: 小窓からスイッチが見えててかわいい

引っこ抜いて差し替えるだけなので簡単

基盤側かスイッチ側の個体差だと思うけど、スイッチのはまり具合がまちまちで、結構力を入れないと外れないスイッチがいくつかあって壊してしまわないかちょっと怖かった。逆にキーキャップを取ろうとしたらスイッチごと抜けてしまうということもあったり。まぁ慎重にやればよっぽどのことがないと壊れないはず。

スト6のトレモやランクマで数時間使ってみたけど、概ね思い描いていた押し心地になって満足。キーストロークが浅くなったので底を打つ時に指にかかる力が大きくなった気はするので手へのダメージは増えるかもしれない。

その後、キーキャップも買って変えてみた。こっちはアリエクで購入。見た目のカスタマイズだけのつもりだったけど、キャップの高さが Rushbox 純正のものよりも低かったので、押した時に天板ギリギリくらいの高さになってさらに押し心地が変わった。あと、表面の加工が梨地?になっててさらさらとした好みの触り心地になったのは嬉しい誤算。

キーキャップも変えた。30φボタンだけ緑

自作キーボードは大変そうだなーと思って HHKB とか Nuphy Air60 とかの既製品を使ってたけど、ホットスワップならスイッチも簡単に交換できるし、キャップだけ変えるのもお手軽に変化が出るので興味ができてきた。

スパンを手作りすることで OpenTelemetry のトレースの基礎を理解する

Mackerel では OpenTelemetry の主要なシグナルのうちのトレースに対応した分散トレーシング機能である Vaxila が使えるようになりました。Web アプリケーションのハンドラをラップすることで簡単に計装するライブラリなどは使ったことがあるのですが、今回は自分でスパンを作りトレースを計装することでより理解を深めてみようと思います。

今回は HTTP リクエストが送信されてからレスポンスが返ってくるまでの時間をトレースしてみようと思います。Mackerel の外形監視ではステータスコード、証明書の有効期限、レスポンスボディのチェックやレスポンスタイムの可視化などはできますが、DNS の名前解決、コネクションの確立、TLS ハンドシェイクの時間などを見ることができないためこれらをスパンとして送信してみます。

概念のおさらい

トレースの概念に関しては OpenTelemetry のドキュメントが簡潔にまとまっていてわかりやすいです。 - トレース | OpenTelemetry - Overview | OpenTelemetry

HTTP リクエスト内部の計装

単純に HTTP リクエストを計装するだけなら otelhttp package を使用し、既存の handler や middleware をラップするだけで完結しますが、今回は DNS の名前解決なども計装したいので otelhttp.NewTransport を用いて Transport を差し替えます。Go で DNS の名前解決などの HTTP リクエスト内部のイベントをトレースするには httptrace package が使用できます(用語が混じってややこしいですがこちらは OTel のトレースとは別物)。ClientTrace の定義を見るとわかるように、名前解決の開始と終了、TLS ハンドシェイクの開始と終了など様々なタイミングにフックを仕込むことができます。

trace package と上記のフックを組み合わせて HTTP の内部処理のスパンを作成します。tracer.Start / tracer.End にオプションを渡すことでタイムスタンプや属性など様々な設定ができます。実際にやってみたのが以下のコード。

package main

import (
    "context"
    "crypto/tls"
    "log"
    "net/http"
    "net/http/httptrace"
    "time"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    "go.opentelemetry.io/otel/trace"
)

func initTracer() func() {
    exporter, err := otlptracegrpc.New(context.Background())
    if err != nil {
        log.Fatalf("failed to initialize exporter: %v", err)
    }

    tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
    otel.SetTracerProvider(tp)

    return func() {
        if err := tp.Shutdown(context.Background()); err != nil {
            log.Fatalf("failed to shut down tracer provider: %v", err)
        }
    }
}

func main() {
    shutdown := initTracer()
    defer shutdown()

    url := "https://example.com"

    client := http.Client{
        Transport: otelhttp.NewTransport(
            http.DefaultTransport,
            otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
                tracer := otel.Tracer("example/client")

                var dnsStart, handShakeStart, connStart time.Time
                var host string

                return &httptrace.ClientTrace{
                    DNSStart: func(info httptrace.DNSStartInfo) {
                        dnsStart = time.Now()
                        host = info.Host
                    },
                    DNSDone: func(info httptrace.DNSDoneInfo) {
                        dnsEnd := time.Now()
                        _, span := tracer.Start(ctx, "DNS Lookup",
                            trace.WithTimestamp(dnsStart),
                            trace.WithAttributes(attribute.String("host", host)),
                            trace.WithSpanKind(trace.SpanKindClient),
                        )
                        span.End(trace.WithTimestamp(dnsEnd))
                    },
                    TLSHandshakeStart: func() {
                        handShakeStart = time.Now()
                    },
                    TLSHandshakeDone: func(state tls.ConnectionState, err error) {
                        handShakeEnd := time.Now()
                        _, span := tracer.Start(ctx, "TLS Handshake",
                            trace.WithTimestamp(handShakeStart),
                            trace.WithAttributes(attribute.Int("CipherSuite", int(state.CipherSuite))),
                            trace.WithSpanKind(trace.SpanKindClient),
                        )
                        if err != nil {
                            span.RecordError(err)
                        }
                        span.End(trace.WithTimestamp(handShakeEnd))
                    },
                    ConnectStart: func(network, addr string) {
                        connStart = time.Now()
                    },
                    ConnectDone: func(network, addr string, err error) {
                        connEnd := time.Now()
                        _, span := tracer.Start(ctx, "Connection Establishment",
                            trace.WithTimestamp(connStart),
                            trace.WithAttributes(
                                attribute.String("network", network),
                                attribute.String("address", addr),
                            ),
                            trace.WithSpanKind(trace.SpanKindClient),
                        )
                        if err != nil {
                            span.RecordError(err)
                        }
                        span.End(trace.WithTimestamp(connEnd))
                    },
                }
            }),
        ),
    }

    ctx := context.Background()
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)

    if err != nil {
        log.Fatalf("failed to create request: %v", err)
    }

    resp, err := client.Do(req)

    if err != nil {
        log.Fatalf("failed to perform request: %v", err)
    }

    defer resp.Body.Close()

    log.Printf("Response status: %s", resp.Status)
}

各フックポイントでタイムスタンプやホスト名、IP アドレスなどを属性として設定しています。また、エラーが起きた場合はスパンのエラーを設定しています。以下の通り HTTP リクエストを親として各内部処理がスパンとして構造化されていることがわかります。

otel-tuiで可視化した様子

otelhttp.WithClientTrace を使う

ある程度コードを書いてからドキュメントの裏どりや世の中で同じことをしている人がいないかを改めて調べてみたところ otelhttptrace というパッケージが用意されていることを知りました。1

このパッケージを使うと http.Clientを作る部分はこれだけで済みます。

   client := http.Client{
        Transport: otelhttp.NewTransport(
            http.DefaultTransport,
            otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
                return otelhttptrace.NewClientTrace(ctx)
            }),
        ),
    }

取得する情報も増えていて、より適切に構造化されていますね。

otelhttp.WithClientTrace を使った場合

初めからこれを使えばよかったんや…という気持ちになりましたが、今回は手でスパンを作ってみるということが目的だったのでグッとその気持ちを飲み込みます。せっかくライブラリがあることを見つけたのでそちらではどういったことをしているのかを少し見てみました。

型定義を見ていくと、トレーサープロバイダーを指定したり、収集する情報のカスタマイズができるようです。デフォルトでは認証用のヘッダーが落とされるのでそれを取得するようにしたり、見つかった情報全てがスパンになることを抑制できたりもする。

clienttrace.go をのぞいてみると、フックの開始・終了処理を共通化して各タイミングで設定したい属性と一緒にそれが呼ばれています。自分で書いた実装では WithTimestamp を使って明にタイムスタンプを設定していましたが、context からスパンを取得して開始・終了のタイミングで同じスパンの Start/End を呼んでいるようです。型定義のところで見たuseSpans フラグによってイベントと属性のみ設定するという処理もありました。

Vaxila に送信してみる

最後に Vaxila にトレースを送信してみます。エクスポーターの設定を以下のように変えるだけです。標準化されたプロトコルの便利さを改めて感じますね。

   exporter, err := otlptracehttp.New(context.Background(),
        otlptracehttp.WithEndpointURL("https://otlp-vaxila.mackerelio.com"),
        otlptracehttp.WithHeaders(map[string]string{
            "Mackerel-Api-Key": os.Getenv("MACKEREL_APIKEY"),
        }),
    )

いい感じに表示されました。

Vaxila にトレースを送信した様子

実際に手を動かしてトレースを計装することにより、スパンの親子関係の設定や属性、イベントの概念のおさらいができたのと、スパンの種類や(今回の実装には出てきませんでしたが)リンクなど触れたことのない概念があるということも知ることができました。


この記事は Mackerel Advent Calendar 2024 7日目の記事でした。

qiita.com