Ktor で例外発生時に Sentry にクラッシュレポートを送る
最近、個人で web アプリケーションを書くのに Ktor (Kotlin の web アプリケーションフレームワーク) を使っています。
Ktor では、1 つ以上の 「インターセプタ (interceptor)」 から成る 「パイプライン (pipeline)」 にリクエストを通すことで、HTTP リクエストに対する処理が実行されます。 各インターセプタがリクエストに対して処理を行う、という形です。 パイプラインについての詳細なドキュメントはまだ未整備な状態 *1 ですが、Application
の説明の方にパイプラインについても簡単に書かれています。
Sentry にクラッシュレポートを送る
今回は、後段のインターセプタで例外が発生した際に Sentry にクラッシュレポートを送るためのインターセプタを書いてみました。
Application
クラスの intercept
メソッド *2 を呼び、インターセプタを追加します。
fun Application.main() { // ... (略) ... Sentry.init(sentryDsn) // Application#intercept メソッドでインターセプタを追加する。 intercept(ApplicationCallPipeline.Call) { try { proceed() } catch (e: Exception) { // 追加情報としてリクエストのエンドポイントの情報を与える。 Sentry.getContext().addExtra( "Request endpoint", "${call.request.httpMethod.value} ${call.request.uri}") Sentry.capture(e) throw e } finally { Sentry.clearContext() } } // ... (略) ...
Sentry
クラスなどは Sentry 公式の sentry ライブラリのものです。
悩み事
Ktor ではコルーチンが使われているため、各リクエストに対して 1 スレッドが割り当てられる、というわけではありません。 一方で、sentry ライブラリのコンテキストマネージャはデフォルトだとスレッドローカル変数を使用してコンテキストが分かれるような作りになっていて、Ktor の処理の中の様々な場所で Sentry のコンテキストを触ろうとするとスレッド違いなどを意識しないといけなくて難しそう、だと思ってます。
上の例では同じスレッド上でコンテキストを触っているので問題ないのですが、もうちょっと込み入ったことをしたくなると不便そうです。 コンテキスト管理をスレッドローカル変数でやるのは悪い文化。
Ktor でクラッシュレポートを Sentry に送るライブラリ
ライブラリもあります。 が、まだスナップショットでしかリリースしてなさそう? なので私は避けました。
本記事でのバージョン
- Kotlin 1.2.10
- Ktor 0.9.0
- sentry 1.6.4
Rust のプレリュード (prelude) とは何か
use std::io::prelude::*;
というコードを見かけて、「プレリュード (prelude) ってなんだっけ」 と思ったので調べた。
Rust のプレリュード
まず、Rust で真にプレリュードと呼ばれるものは std::prelude
というモジュールらしい。
Rust comes with a variety of things in its standard library. However, if you had to manually import every single thing that you used, it would be very verbose. But importing a lot of things that a program never uses isn't good either. A balance needs to be struck.
The prelude is the list of things that Rust automatically imports into every Rust program. It's kept as small as possible, and is focused on things, particularly traits, which are used in almost every single Rust program.
std::prelude - Rust
std::prelude
モジュールに含まれるものは、すべての Rust プログラムで自動的にインポートされる。 このような仕組みになっているのは、よく使うものも全て手動でインポートする必要があるならばコードが煩くなってしまうため。
他のプレリュード
Rust の真のプレリュードは上に述べた通りだが、それ以外にも、複数の型を使用する際のインポートをより便利にするパターンとしてプレリュードの名が使われている。
std::prelude - RustOther preludes
Preludes can be seen as a pattern to make using multiple types more convenient. As such, you'll find other preludes in the standard library, such as
std::io::prelude
. Various libraries in the Rust ecosystem may also define their own preludes.The difference between 'the prelude' and these other preludes is that they are not automatically use'd, and must be imported manually. This is still easier than importing all of their constituent components.
例えば std::io::prelude
というモジュールは、IO 周りでよく使う型のインポートを楽にするためのモジュールである。 以下のようにして使われる。
use std::io::prelude::*;
真のプレリュードとは違い、自動的にインポートされるわけではなく、あくまで手動のインポートを楽にするためのパターンとのこと。 (なので、prelude
という名前じゃなくても挙動的には良さそうである。)
CircleCI 2.0 で複数ファイルからキャッシュキーを生成する便利な方法
CircleCI 2.0 のウリの一つが、1.0 より柔軟になったキャッシュ機能である。 キャッシュのキーを自分で決定し、ファイル群をキャッシュすることができる。 そして、キャッシュのキーの決定には、ブランチ名を使ったり、指定のファイルのチェックサムを用いたりできる。 たとえば pom.xml のファイルの内容に応じてキーを変更したい場合は m2-{{ checksum "pom.xml" }}
という風に記述する。 (下記参考ページのサンプルより。)
- 参考 : Caching Dependencies
さて、ファイルの内容が異なっていたらキーを変えたい、というとき、その対象のファイルが複数個存在することが多いと思う。 記法的には m2-{{ checksum "pom.xml" }}-{{ checksum "..." }}-{{ checksum "..." }}
という風に複数ファイルを指定できるが、もっと書きやすくしたい。
そこで、私は 「キーの計算対象のファイル群の md5sum
コマンドの出力を集めたファイルを 1 つ用意して、それをキー計算に用いる」 ということをしている。
具体的には、以下のような感じである。 Maven の依存ライブラリをキャッシュするため、まず、.mvn/wrapper/maven-wrapper.properties やプロジェクト中の全ての pom.xml に対する md5sum
の出力を ~/cache-key-source-m2 に書き出している。
- run: name: Calculate cache key for Maven dependencies command: | { md5sum .mvn/wrapper/maven-wrapper.properties md5sum $(find . -name 'pom.xml' | sort -r) } > ~/cache-key-source-m2
そして、~/cache-key-source-m2 ファイルだけを対象にキーを計算している。
- restore_cache: keys: - m2-deps-v1-{{ checksum "~/cache-key-source-m2" }} - m2-deps-v1-
Android のプロジェクトなどでも、settings.gradle と全ての build.gradle を対象にしてキー計算をしたいということが多い。 そのような場合でも活用できるだろう。 (より良い記述方法があったら教えてください!)
関連ページ
- Let's Improve Your and Co-workers' DX ~ CI Improvement Tips ~ - おでーぶでおでーぶ : 「md5 hash of multiple files」 にて、
while
を使った書き方が紹介されている。
Docker コンテナなどの Linux 環境でタイムゾーンを指定する (TZ 環境変数)
やりたいこと
調べてよく引っ掛かるのは /etc/localtime (などのファイル) を変更する方法
「Linux timezone」 みたいな検索ワードで検索すると、/etc/localtime (などのファイル) を書き換える方法が多く見つかる。
これらの方法は、システムのタイムゾーン設定を変更するもので、ディストリビューションごとに詳細な設定方法は違っていたりする。
Docker コンテナ内のプロセスのための設定などは TZ
環境変数を使うのが良さそう
別の方法として TZ
環境変数を設定するというものもある。 これは POSIX 標準の方法っぽい。
You should not normally need to set
TZ
. If the system is configured properly, the default time zone will be correct. You might setTZ
if you are using a computer over a network from a different time zone, and would like times reported to you in the time zone local to you, rather than what is local to the computer.
とのことで、おそらく Docker コンテナ内のプロセスのためのタイムゾーン設定などは TZ
を設定する方法が適していると思われる。
ちなみに設定する値については以下のように書かれている。
If characters begins with a slash, it is an absolute file name; otherwise the library looks for the file /usr/share/zoneinfo/characters. The zoneinfo directory contains data files describing local time zones in many different parts of the world.
つまり、TZ=Asia/Tokyo
みたいに設定するか、TZ=/usr/share/zoneinfo/Asia/Tokyo
みたいに設定するかのどちらかを選べる。
docker
のコマンドオプションで設定する場合は、docker -e TZ=Asia/Tokyo -it --rm --name xxx image/xxx
みたいな感じ。
CircleCI 2.0 のジョブ定義であれば、以下のようにして設定できる。
jobs: <job name>: environment: TZ: "/usr/share/zoneinfo/America/Los_Angeles"
参考
- Migrating from 1.0 to 2.0 - CircleCI
- Best way to set the timezone · Issue #12084 · moby/moby · GitHub
- glibcを更新しても大丈夫な「正しい」タイムゾーンの設定方法 (2/3追記あり) - めもおきば
- タイムゾーンの設定方法をメモ(RHEL6, RHEL7, Ubuntu編) | Siguniang's Blog
- dockerコンテナのタイムゾーンを簡単に設定する - 隙あらば寝る
- Setting timezone from terminal - Ask Ubuntu
- DockerのTime zoneをJSTに設定する |
Java Persistence API (JPA) の JPQL で集計処理を書く (Spring Data JPA)
Spring Data JPA を使っていて、集計処理をどのように書くのが良いかわからずに調べた記録。 (例は Spring Data JPA 固有の話ではあるが、JPQL の知識は Spring Data JPA 以外での JPA でも使える。)
Spring Data JPA での集計の例
リポジトリにメソッドを定義して @Query
アノテーション使うことで、JPQL (Java Persistence query language) によるクエリを記述できる。 これを用いて、例えば FooRepository
に Foo
に関する集計処理を記述できる。
@Query(value = """ SELECT NEW com.example.AggregationResult( FUNCTION('year', foo.date), COUNT(foo) ) FROM Foo foo LEFT JOIN foo.bar bar LEFT JOIN foo.baz baz WHERE foo.targetId = ?1 AND (bar.content IS NOT NULL OR baz.content IS NOT NULL) GROUP BY FUNCTION('year', foo.date) """) fun aggregateCountPerYear(targetId: Long): List<AggregationResult>
上のコードは Kotlin で記述されたメソッド定義の例である。 (FUNCTION('year', foo.date)
が 2 箇所にあるのが微妙だし仕様上正しく動くものなのかわからないので何とかしたかったけど、どう書くのがいいかわからなかったのでこの形になっている *1。 誰か詳しい人が居たら教えてください><)
上記の例に含まれる JPQL の知識を下記に述べる。
集計に使える JPQL の知識
参照した JPA 仕様
学び
SELECT
句内のコンストラクタ式 (JPA 2.2 仕様の 4.8.2 節)
JPQL の SELECT
句の項目として Java クラスのコンストラクタを記述できる。 集計結果をオブジェクトで返したいときに便利。 ちなみに対象のクラス名は完全修飾名で指定する必要がある。
データベース関数の実行 (JPA 2.2 仕様の 4.6.17.3 節)
データベースの関数を FUNCTION(function_name, arg1, arg2, ...)
の形で記述できる。 JPQL で定義されていない集計関数を用いたい場合や、ユーザー定義の関数を利用したい場合などに便利。
LEFT JOIN
(JPA 2.2 仕様の 4.4.5.2 節)
LEFT JOIN
と LEFT OUTER JOIN
はシノニムである。 JPA においては、エンティティ定義でエンティティ間の関連を定義して、JPQL では 単純に下記のようにプロパティ名の記述だけで JOIN
を記述できる。
FROM Foo foo JOIN foo.bar bar
ただし、上記のコードでは foo.bar
が null
となるような Foo
は処理対象から外れてしまう。 foo.bar
が null
でも処理対象とするために LEFT JOIN
を使用する必要がある。 普通に SQL を書く場合には LEFT JOIN
を使うのは自然だと思うが、JPQL だと JOIN
の条件を書かなかったり、Foo
のエンティティを普通に引っ張ってくるときは bar
が null
でも取得できたりするので、LEFT JOIN
が必要なことをうっかり忘れがちになりがちな気がする。 注意が必要。
参考
- JavaEE7をはじめよう(4) - JPAクエリ(その1) JPQL - エンタープライズギークス (Enterprise Geeks) : JPQL の全体的な話。 参考になる。
- わかりやすい JPA(7)関数と集計クエリ : JPQL での集計の話。 参考になる。
- JPQLの集計関数に価値はあるか? - taediumの日記 : 集計には JPQL ではなく SQL を使った方が良いのではないかという話。 確かに……。 JPQL を使うとエンティティ定義を扱えるので、JPA の世界の中で集計クエリを書けるという利点はあるような気がする。 (多少メンテナンス性は良くなる気がするので、JPQL で困らないなら JPQL で書くと良さそうな気はしている。)
*1:テストは書いてあるのでまあ大丈夫かなという……。