Location via proxy:   
[Report a bug]   [Manage cookies]                

Emacs Lisp で JST → UTC の変換

こんな感じの関数が多分シンプル?

(defun jst-to-utc (time-str) 
  (format-time-string
   "%Y-%m-%dT%H:%M:%S%z"
   (date-to-time time-str) 0))

(jst-to-utc "2025-02-04T21:57:14+09:00")
;; => "2025-02-04T12:57:14+0000"

M-x で呼び出し可能にするにはこう

(defun jst-to-utc (time-str)
  (interactive "sJST Time Str: ")

  (message (format-time-string
            "%Y-%m-%dT%H:%M:%S%z"
            (date-to-time time-str) 0)))

仕組み

Emacs で時刻を扱う際にはLispタイムスタンプという時刻をS式で表現する値を使う。

Time of Day (GNU Emacs Lisp Reference Manual)

例えば↑で変換した時刻をLispタイムスタンプ値にするとこうなる。

(date-to-time "2025-02-04 21:57:14+09:00")
;; => (26528 55792)

この値に対して format-time-string でフォーマットしてやると好きなタイムゾーンにできる

Time Parsing (GNU Emacs Lisp Reference Manual)

ちなみにLispタイムスタンプ値にから値を取るには decode-time という関数がある。時刻の値を細かく扱いたい場合はこの関数を使うとよさそう。 今回はLispタイムスタンプ値をそのまま別のタイムゾーンの文字列にフォーマットしたかったので登場しない*1

Time Conversion (GNU Emacs Lisp Reference Manual)

(decode-time (date-to-time "2025-02-04 21:57:14+09:00"))
;; => (0 0 0 4 2 2025 2 nil 32400)

*1:最初 ChatGPT に聞いたらこれを使うコードを出されて混乱した

Emacs のトレース情報を Mackerel(Vaxila) に送りたかった……

この記事は はてなエンジニア - Qiita Advent Calendar 2024 - QiitaMackerel - Qiita Advent Calendar 2024 - Qiita 6日目の記事です。昨日は id:pokutuna さんの わんぱくな JSON ストリームパーサーを見る日 - ぽ靴な缶id:kmuto さんの https://mackerel.io/ja/blog/ entry/2024/12/05/090000 でした。

Mackerel は最近オブザーバビリティに力を入れているので、それに絡めたことをやろうとしたのですが、いいネタが出るところまで出来なかった、、という記事を書きます。

やりたかったこと

  • 普段 Emacs を使っているが、lsp 周りの動作がなんかひっかかる時があり、ボトルネックを調べたい
  • Emacs にもProfiling機能はあるけど、もっといい感じの可視化出来ないだろうか
  • 関数呼び出しを分散トレーシングに見立ててやると、その辺りのツールを使えて便利なんじゃないか
  • OpenTelemetry 周りの技術や概念にあまり触れてこなかったし、いい機会だし学びながらやってみよう

こんな気楽な感じにとらえてネタを考えていました。

Emacs の lsp 拡張機能のログを取る

送る情報を作らないことには始まらないので、送りたい情報のログを出すところから始めていました。 基本的には、Emacs の lsp 拡張の関数が呼び出された時の実行開始・終了時刻、引数や実行結果がわかるとよいだろうとして以下の関数を用意しました。

ちなみに、普段 Emacs を使っていますが、Lisp については初心者なので 今回のコードはほぼ ChatGPT ベースですなので、妥当な Lisp コードになっているとは限りません*1

(defun my/create-otel-trace-json (trace-id span-id parent-id function-name args result start-time end-time)
  (json-encode
   `(("resourceSpans" . [
                         (("resource" . (("attributes" . [
                                                          (("key" . "service.name")
                                                           ("value" . (("stringValue" . "emacs-logger"))))])))
                          ("scopeSpans" . [
                                           (("scope" . (("name" . "log-function-call")
                                                        ("version" . "1.0.0")))
                                            ("spans" . [
                                                        (("traceId" . ,trace-id)
                                                         ("spanId" . ,span-id)
                                                         ("parentSpanId" . ,(or parent-id ""))
                                                         ("name" . ,function-name)
                                                         ("startTimeUnixNano" . ,start-time)
                                                         ("endTimeUnixNano" . ,end-time)
                                                         ("attributes" . [
                                                                          (("key" . "function.args")
                                                                           ("value" . (("stringValue" . ,(format "%s" args)))))
                                                                          (("key" . "function.result")
                                                                           ("value" . (("stringValue" . ,(format "%s" result)))))
                                                                          ])
                                                         )]))]))]))))

最終的には OpenTelemetry の仕様に沿ったデータを送るので、ログフォーマットは以下を参考にしました。

次に、関数を呼び出したらログを記録する関数を用意します。 デバッグのためにバッファに出力させています。

ID 生成関数は ChatGPT に投げたらそれっぽいのを返してくれて、便利ですね。

(defvar my/log-current-span-id nil)
(defvar my/log-current-trace-id nil)

(defun my/generate-random-id ()
  (let ((id-length 16))
    (apply 'concat
      (mapcar (lambda (_) (format "%x" (random 16)))
        (make-list id-length nil)))))

(defun my/log-function-call (original-function &rest args)
  (let* ((trace-id (or my/log-current-trace-id (my/generate-random-id)))
         (span-id (my/generate-random-id))
         (parent-id my/log-current-span-id)
         (start-time (current-time-unix-nano))
         (function-name
          (if (symbolp original-function)
              (symbol-name original-function)
            (if (subrp original-function)
                (subr-name original-function)
              "unknown-function")))
         (result (let ((my/log-current-trace-id trace-id)
                       (my/log-current-span-id span-id))
                   (apply original-function args)))
         (end-time (current-time-unix-nano))
         (trace-json (my/create-otel-trace-json trace-id span-id parent-id function-name args result start-time end-time))
         ;; デバッグ用にバッファに出力
         (log-buffer (get-buffer-create "*Function Call Log*")))
    (with-current-buffer log-buffer
      (goto-char (point-max))
      (insert trace-json)
      (insert "\n"))
    result))

これを計測したい関数が呼ばれたら実行するようにしていきます。 Emacs には advice という関数呼び出しにフックして処理を追加できる機能があるのでそれを使います。

(defun my/trace-functions-by-prefix (prefix)
  (interactive "sPrefix: ")
  (let ((count 0))
    (mapatoms
     (lambda (symbol)
       (when (and (functionp symbol)
                  (string-prefix-p prefix (symbol-name symbol))
                  (not (advice-member-p #'my/log-function-call symbol)))
         (advice-add symbol :around #'my/log-function-call)
         (setq count (1+ count)))))
    (message "Tracing enabled for %d functions with prefix '%s'." count prefix)))

この関数により M-x my/trace-functions-by-prefix で呼び出したい関数の prefix を入力することで計測処理を追加できます。 例えば lsp-mode であれば lsp- の prefix を入力します。

以下は適当な React プロジェクトのファイルを開いたときのログを抜粋です。

{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"emacs-logger"}}]},"scopeSpans":[{"scope":{"name":"log-function-call","version":"1.0.0"},"spans":[{"traceId":"53b5ecdedb7b3931","spanId":"b680e98d20ca1f0c","parentSpanId":"f7d4bdd02c75fb5f","name":"lsp-enable-which-key-integration","startTimeUnixNano":113601885044340118973000,"endTimeUnixNano":113601885044340119445000,"attributes":[{"key":"function.args","value":{"stringValue":"nil"}},{"key":"function.result","value":{"stringValue":"((tsx-ts-mode))"}}]}]}]}]}
{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"emacs-logger"}}]},"scopeSpans":[{"scope":{"name":"log-function-call","version":"1.0.0"},"spans":[{"traceId":"53b5ecdedb7b3931","spanId":"0ac6c341cd4cd825","parentSpanId":"f7d4bdd02c75fb5f","name":"lsp-ui-mode","startTimeUnixNano":113601885044340168280000,"endTimeUnixNano":113601885044340168401000,"attributes":[{"key":"function.args","value":{"stringValue":"nil"}},{"key":"function.result","value":{"stringValue":"t"}}]}]}]}]}
{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"emacs-logger"}}]},"scopeSpans":[{"scope":{"name":"log-function-call","version":"1.0.0"},"spans":[{"traceId":"53b5ecdedb7b3931","spanId":"f7d4bdd02c75fb5f","parentSpanId":"","name":"lsp","startTimeUnixNano":113601885044338343217000,"endTimeUnixNano":113601885044340172594000,"attributes":[{"key":"function.args","value":{"stringValue":"nil"}},{"key":"function.result","value":{"stringValue":"LSP :: Connected to [ts-ls:69060/starting /home/benevolent0505/develop/tmp/react/react-sample][graphql-lsp:69061/starting /home/benevolent0505/develop/tmp/react/react-sample]."}}]}]}]}]}

それっぽくなってきた。 ここまで出来たら後は送るだけのはず。

分散トレーシングサービスに送りたい……

Mackerel は分散トレースサービスである Vaxila が使えるようになっています。 Vaxila に今回のトレースデータを送って可視化してみたいですが、当然 Emacs Lisp に OpenTelemetry の SDK はないので、HTTP リクエストを送る関数を書いて簡易でやってみることにしました。

(defun my/send-to-vaxila (trace-data)
  (let* ((url "https://otlp-vaxila.mackerelio.com/v1/traces")
         (url-request-method "POST")
         (url-request-extra-headers '(("Content-Type" . "application/json")
                                      ("Accept" . "*/*")
                                      ("Mackerel-Api-Key" . "API_KEY")))
         (url-request-data trace-data))
        (message "Sending data to vaxila: %s" trace-data)
    (condition-case err
      (let ((response-buffer (url-retrieve-synchronously url)))
        (if response-buffer
          (with-current-buffer response-buffer
            (goto-char (point-min))
            (if (re-search-forward "^HTTP/[0-9.]+ \\([0-9]+\\)" nil t)
              (let ((status-code (string-to-number (match-string 1))))
                (if (= status-code 202)
                  (message "Successfully sent data to vaxila.")
                  (message "Failed to send data: HTTP %d" status-code)))
            (message "Failed to parse HTTP response.")))
          (message "No response from vaxila.")))
          (error (message "Error occurred while sending to vaxila: %s" err)))))

しかし、送る部分が上手く成功せず断念しました……。

敗因

全体的にネタが見切り発車過ぎたのはありますが、Otel 周りの技術をわかっていなかったこと、Emacs Lisp との格闘に時間をかけすぎたことが敗因かと思います。Emacs Lisp のコードもミスっている可能性があり、流石に準備不足でした……

この後試してみたいこと

調べている内に otel-cli という cli で OpenTelemetry に対応しているサービスにテレメトリーを送るツールを見つけたので、これを利用して、外部コマンド呼び出しで送るのを試してみるのはよいかと思いました。

github.com

また otel-collector に集約させると Emacs 側の実装もシンプルになる? 一般的には otel-collector を経由して送ることが多そうなので、この方法も試してみたいと思います。

あと今回は流石に ChatGPT に頼りすぎたと思ったので、Lisp 力は上げたいですね……。

感想

最終的な目的は果せませんでしたが、デバッグのためにログを出している際も裏側の関数呼び出しの様子や、そもそも呼び出されている関数の多さがわかり、そこは面白かったです。 このように普段見れていない情報を見れるようにするだけででも気づきがあるので、まず見れるようにすることだけでも第一歩なのだと思いました。

自分達が普段運用しているサービスだけでなく、身近なツールのオブザーバビリティを上げていきたいですね。

*1:今回の敗因の一つは ChatGPTに頼りすぎたこともありそう……

「ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本」を読んだ

読んだ理由

積読してあった本の消化のためと、最近設計について考えていた時期で、課題意識を持つタイミングが重なっていたこともあった。

本の感想

非常に読みやすい本だと感じた。

以前「エリック・エヴァンスのドメイン駆動設計: ソフトウェアの核心にある複雑さに立ち向かう」や「 実践ドメイン駆動設計」を読んでいたおかげもあるかもしれないが、具体的かつ現代的なコード例を使って概念を説明しているため、理解しやすかった。 特に、例として実装するアプリケーションが、ユーザー登録やTwitterのコミュニティ機能といった、Webアプリケーション開発者とって身近なものだったことも大きかった。

また、ドメイン駆動設計について説明する構成も秀逸だと思った。例えば「まずうまくいかない事例を示し、それを解決するための新しい概念を紹介する」といった流れが非常にスムーズだった。

実際読んでいると、トランザクションやパフォーマンスに関する疑問が浮んだときに、直後の章でそれを解決する内容が紹介されたことがあり、巧い構成だと感じた。

一方、ドメインモデルを作り上げていく部分については少し物足りなさを感じた。 「エリック・エヴァンスのドメイン駆動設計」ではこの本の章で言う Chapter 15 しか取り上げていないと言ってもいいくらい書いてあったので、この本だけでは単なるパターン集として読まれてしまう可能性もあると感じた。

タイトルにも「入門」とあるように、この本でドメイン駆動設計についての雰囲気やコードでどう表現するかを掴んだ後に、「エリック・エヴァンスのドメイン駆動設計」などの別の書籍や資料、実践に繋げていくのがよいのだろうと思った。

自分もまた読み返したい気持ちになった。

この後やりたいこと

普段触っている言語で実践し、理解を深めたいと思った。

今一つ納得できていない部分は、レポジトリの辺りで出てくる ImMemoryHoge について。 抽象に依存させるのはわかるが、InMemoryHogeRepository は大抵の場合不要なんじゃないかと思うが、どうなんだろうか。 実装の先がマイクロサービスや SaaS など外部 API にリクエストを送るものだと話が違ってくるかもしれないが、ローカルでも動かせるDBを扱っている限りは別にいらないだろうと思っている。 テストもテストダブルを作るライブラリを使っておけばいいから、ImMemoryHoge はいらないと思っているけどどうなんだろうな~。

という辺りを実際に手を動かして擦り合せていきたい。

最後に

読んで一番よかったことは、ドメインモデルを作り上げる重要性を思い出せたこと。 最近はテクニカルなことや他の課題に目が向きがちで、設計について疎かになりがちだったと思う。 今回の読書を通して、モデリングの重要さを再認識できるよいきっかけになったのが一番の収穫だった。

東京Emacs勉強会 サマーフェスティバル2024に参加した

tokyo-emacs.connpass.com

東京Emacs勉強会 サマーフェスティバル2024に参加した。

Emacs の勉強会は 2017 年の Emacs 実践入門出版イベントに参加したきりなので、ほぼ7年ぶり。

正直どれくらい人集まるのかと思っていたけど、当日になると50人近く集まるイベントになっていて、「Emacs ユーザー思ったより全然いるじゃん!」というのを実感出来た。 発表者の方もすごくて、特に日本語訳のドキュメントにいつもお世話になっているayatakesiさんの話が聞けたのがすごくよかった。

参加者も ITエンジニアの方からライターの人まで幅広くて、皆それぞれの Emacs の使い方があるのだと思った。

参加して一番よかったのは、大規模じゃないけど参加者同士でわいわい盛り上がっている勉強会の雰囲気を感じられたのがとてもよかった。 懇親会で「このパッケージ便利なんですよ→何それ知らなかった、入れます!」と、さっとPCを開いてインストールするみたいなやり取りがひさしぶりすぎて、こういうカジュアルな交流が出来たのが楽しかった。

運営の方々、楽しいイベントを開催していただきありがとうございました!

今後もちょくちょく顔を出して交流したいし、Emacsをもっと使い倒していきたい。 次回は Emacs オクトーバーフェス? らしいので、楽しみに待っています。

(この記事も Emacs を使って書かれました)

flaky test の調査のためにテスト中で使う変数でもログに出すようにする

というのを最近試みている。

これまで遭遇してきた flaky test の多くは比較的単純な原因だった。コードを少し眺めたり、乱数が原因で落ちたテストのシード値を指定するだけで再現できることが多かったため、あまり困っていなかった。

しかし、最近遭遇したテストは、並行で実行されるテストで作られるデータと競合していたため、調査が難航していた。 周辺コードや既存のログを読んでも原因が全く分からなかったため、テストの前後で変数をダンプするログを追加して、しばらく CI の様子を観察していた。 すると、調査が一気に進み、flaky test の修正も可能になりました。

この件から、普通のアプリケーションの不具合調査の際と同じように、情報を増やすためにログを仕込むテクニックが flaly test の修正でも有効であることを認識してきた。 特に、テスト実行中だとデバッガもないので、変数の中身が出るくらいで情報は大幅に増えることがわかった。

違う話題だと、世の中には flaky test の実際の修正方法についてはあまり言及されていなさそうと思って調べてみたら、以下のブログを見つけた。 Railsプロジェクト以外でも共通するパターンがまとめられており、参考になりそう。

note.com

エスパー力を発揮したり怪しいところにログを仕込むなどして見つけるしかないこともあります。

やっていきましょう。

Emacs Lispで感じた書き散らすプログラミング体験

この記事は、はてなエンジニア Advent Calendar 2023の2024年1月16日の記事です。

昨日は id:fxwx23 さんによる「Simulator.app の「Stay On Top」をキーボードショートカットで切り替える」でした。 かゆい所に手が届かないこと、よくありますよね。その際にシュッとスクリプトを書いたりして不便を解消する姿は見習いたいと思います。

始めに

自分は普段テキストエディタEmacs を使っています。 EmacsEmacs Lisp というプログラミング言語でエディタの設定を記述できますが、今まで設定を記述する以外の目的で Emacs Lisp を書いたことがなく、プログラムっぽいプログラムを書いたことはありませんでした。

ふとこのことが気になって、年末年始の休みに簡単なプログラムを作ってみました。

実際に書いてみると、他の言語ではあまり体験したことのないようなプログラミング体験が出来ました。 ここでは Emacs Lispプログラミング言語として書いたときに得たプログラミング体験と、その感想について書いています。

Emacs Lisp でのプログラミング体験

LTSV 文字列をリストに変換する関数の実装を進めることを例として見ていきます。 以下は実装の一例です。Lisp について知らなくても、雰囲気でも何をやっているかがわかると思います。

(defun parse-ltsv-string (ltsv-string)
  "引数の LTSV の文字列を リストのリストにする ((key1 value1) (key2 value2))."
  (let ((values '()))
    (dolist (pair (split-string ltsv-string "\t"))
      (let ((key-value (split-string pair ":")))
        (push key-value values)))
    values))

(parse-ltsv-string "key:value\tname:miki_bene")
;; => (("name" "miki_bene") ("key" "value"))

この関数の実装過程で 4行目の (split-string ltsv-string "\t") の返り値が実際にどんな値が返るのか、確認してみたかったとします。 他の言語なら REPL を起動したり、ブラウザ上で実行できる Playground で試したりするところですが、Emacseval-last-sexp *1というコマンドを使うことで Emacs 上で実行が出来ます。

(defun parse-ltsv-string (ltsv-string)
 ...
)

;; 以下のコードの括弧の末尾にカーソルを配置し、
;; C-x C-e キーバインドで eval-last-sexp コマンドを実行する
(split-string "key:value\tname:miki_bene")
;; => ミニバッファに ("key:value" "name:miki_bene") が表示される

このように、編集しているプログラムから全く離れずに書いたコードを実行できます。

また、eval-last-sexp はどこでも実行できるので、例えば関数の中で全然違う処理をいきなり書いて実行することも出来ます。

(defun parse-ltsv-string (ltsv-string)
  ...

  ;; dolist でループ処理を行う際の動きを見たいので
  ;; *Messages* バッファに文字列を出力する
  (dolist (pair (split-string "key:value\tname:miki_bene" "\t"))
    (message "%s" pair)) ;; ここで C-x C-e コマンドを実行する
  ;; => *Messages* バッファに
  ;; key:value
  ;; name:miki_bene
  ;; nil
  ;; が出力される
  
  ;; 以下は書いている途中の処理
  (let ((values '()))
  ...
)

実は最初のコードもコマンドラインで実行した結果ではなく、Emacs 上で評価した結果を貼りつけているだけでした。

感想

例のように、Emacs Lisp では今書いているプログラムからほとんど離れずに処理を試したり、関数を実行できます。

実際に Emacs Lisp を書いていた際は、関数定義中の処理の中に具体的な値を入れて試す→動くことを確認したら値を変数に置き換えて続きを書き進める→また確認したくなったら具体的な値を入れて試す……を繰り返しながら実装を進めていました。 この繰り返しをすると気づいたらプログラムが出来上がっており、書いたプログラムが動くことの確信がほぼ持てているといった状態になっていました。

他のプログラミング言語で実装を進める際も似たようなことは行っているはずですが、Emacs Lisp の場合は処理の記述と実行の間がとてもシームレスで、絶え間無くプログラムを書き進められる感覚がありました。

もちろん Lisp 自体でつまずくことはあったので、言うほど絶え間無くではありませんでした。 ただ、書いてすぐに実行してフィードバックが返る素朴な楽しさがありました。

関数の中で全然違う処理を書いて実行できるのは滅茶苦茶だなと思いつつも、好きな場所に好きに書いても実行できる感覚は真っ白な自由帳に落書きを書いているみたいで、それも楽しくありました。

他のエディタで類似の機能はないのか

Emacs Lisp 以外で同様のものはないかと探してみると、VSCode の拡張に似た機能がありました。 選択した範囲を REPL に流し込み実行できるといったもので、割と感覚は近いかもしれません。

ただし Emacs Lisp の場合は評価した結果が残るので、以前評価されていれば選択していない部分の関数定義やライブラリ読み込みも使えるのは大きな違いかもしれません。 評価が残ってしまうのは意図しない結果になる時もありますが、その場で実行する場面においては基本利便性の方が高いだろうと感じています。

まとめ

Emacs Lisp を書いて、他の言語ではあまり体験できない実装体験が出来ました。 普段の業務では Perl や Go, TypeScript を書いています。これらの言語が悪いわけではない(むしろ静的型付けの方が好ましい)ですが、普段はあまり感じない書き散らす楽しさを久しぶりに感じました。

たまにはこういった自由帳のような言語で遊んでみるのもよいかもしれません。

はてなエンジニア Advent Calendar 2023、明日は id:hagihala さんです!

YAPC::Kyoto 2023 に参加してきた

3月18, 19日の2日間、YAPC::Kyoto 2023 に参加してきました。

yapcjapan.org

4年ぶりのリアルイベント

ここまで大きなリアルイベントは2019年に参加した YAPC::Tokyo 2019 以来で4年ぶりの参加だった。

リアルの会場に行って人と話すイベントはやっぱり良いものだなと思い直した2日間だと思った。

発表を聞いたり他の参加者の人と話したりすると「自分も何かしたいな」とか「コード書きたいな」など不思議とやる気になれる。

これが刺激になるって感覚なのかもしれないけど、この感覚を本当に久し振りに得ることが出来てとてもよかったと思う。

会話しに行く感覚を忘れていたことの若干の心残り

色々な人と話をしたものの以前会ったことのある人とか終わった後の別の場での会話がメインだった。 やっぱり会場で話すってことをもう少しできるとよかったなと思っていて、そこがちょっと心残り。

いきなり発表を聞いた後隣の人に話しかけられるといいけれど、やっぱり何も無しで行くのはちょっと抵抗感があるので、懇親会みたいな場があると嬉しい。

コロナ以前の他のイベントだけど、確か ScalaMatsuri でコーヒーを無限に飲めるスペースがあって、そこで隣にいる参加者と話すみたいな体験を思い出した。

あれはすごくよくて、コーヒーを飲む口実と場所があるお影で話しかける心理的ハードルを下げられる仕組みが整っていて、積極性が無い人間にも話しかける道具を与えてくれる感じがしてすごくよかった。

こういうことがが出来るといいけどまだ情勢的に微妙そうなのもすごくわかる。もしできればいつか是非……。

運営の皆様への感謝

これだけのイベントを準備してくださった運営の皆様、本当にありがとうございました!!!!!!!!!!!!!!!!!!!!!

イベント開催のための具体的な苦労は考えが及ばないことが多いけど、コロナ禍が落ち着いてきそうとはいえ本当に実施出来るのか、また延期になったりしないのか*1って不安はどうしてもあったと思うし、向こう1週間くらいまだ不安が残ると思う。 その中でも準備を進めてくれて開催してくださったのは本当にありがたかったです。そのお陰で上のような体験が出来たので本当に本当に感謝しかないと思っています。

繰り返しになりますが、YAPC::Kyoto 2023の準備してくださった運営・スタッフの皆様、本当にありがとうございました!!!!!!!!!!!!!!!!!!!!! 次の Hiroshima も行くぞ!!!!!!!!!!!!!!!