abyo software 合同会社から、CloudWatch Logs の請求を 1〜2 桁圧縮することを目的とした OSS S4 Logs をリリースしました。 Apache-2.0 で公開しています。
- リポジトリ: https://github.com/abyo-software/s4-logs
- 最新リリース: v1.1.1 (2026-06-27)、 直前の v1.1.0 (2026-06-15) と合わせて紹介します
- 言語: Rust (静的リンク musl バイナリ配布)
- 兄弟プロダクト: S4 (S3 の料金を圧縮するほう)
なんの課題を解くのか
CloudWatch Logs の請求書を眺めると、ほとんどの場合「取り込み (ingest) $0.50/GB」が支配的です。 保管 ($0.03/GB·月、しかも gzip-6 圧縮後のバイト数に対して) はその一部にすぎません。 そして取り込まれたログのほとんどは「書いて、二度と読まれない」。 高い ingest 単価を払って読みもしないデータを CloudWatch に山積みしている、というのが典型的な状況です。
S4 Logs はこれを 2 つの独立したモードで攻めます。
Mode A — Drain: 既存ログを S3 に退避する
FilterLogEvents で既存ロググループからログを引き出し、 標準 RFC 8878 の zstd フレームとして S3 に書き出します。 1 時間 UTC アライン窓を単位とし、各窓ごとにマニフェストを残します。 同じウィンドウを再実行してもマニフェスト済みなのでスキップされる (冪等)。
# ドライランで「いくら浮くか」だけ見る
s4logs plan --all
# 実際にアーカイブ
s4logs drain --log-group /aws/lambda/payments \
--bucket my-archive-bucket --prefix s4logs
# Glacier Instant Retrieval に直送 ($0.004/GB·月、ミリ秒応答)
s4logs drain --log-group /aws/lambda/payments \
--bucket my-archive-bucket --prefix s4logs --storage-class glacier-ir
# CloudWatch 側のリテンションを縮める (全窓アーカイブ済みのときだけ通る fail-closed ゲート)
s4logs drain --log-group /aws/lambda/payments \
--bucket my-archive-bucket --retention-days 7 --apply-retention
Mode A だけでは「すでに払った ingest は戻ってこない」 ─── 削減できるのは保管料金です。 ただし S3 Standard で約 50–70%、Glacier IR で約 90% カット、 さらにリテンションを縮めれば CW 側の保管料金そのものが消えます。
Mode B — Bypass: ingest を回避する
ここが本命です。 CloudWatch Logs の AWS JSON 1.1 API サブセット (PutLogEvents, CreateLogGroup, CreateLogStream, DescribeLogGroups, DescribeLogStreams) を喋るゲートウェイを立て、 Fluent Bit / CloudWatch Agent / SDK の エンドポイントを差し替えるだけで取り込みを横取りします。 アプリ側のコード変更は不要。
# routing.toml ─── first-match wins
default_action = "s3" # s3 | cloudwatch | both | drop
[[rule]]
log_group = "/aws/lambda/payments-*"
action = "cloudwatch" # アラート系だけは CW にも流す
Fluent Bit はこれだけ:
[OUTPUT]
Name cloudwatch_logs
Match *
region us-east-1
log_group_name /app/api
log_stream_prefix node-
auto_create_group On
endpoint http://s4logs-gateway.internal:8080
--wal-dir を渡せば ack 前に fsync されるので、 クラッシュしても at-least-once で復元されます。
ざっくりいくら浮くのか
AWS 公式の list price (2026-06、us-east-1) で 1 TiB/月のログを想定:
| CloudWatch そのまま | Mode A (S3 Standard) | Mode A (Glacier IR) | Mode B | |
|---|---|---|---|---|
| ingest | $512 | $512 (既払、回収不能) | $512 (同) | $0 |
| storage | ~$7.7/月 (gzip-6 換算) | ~$3.8/月 | ~$0.66/月 | 同左 |
Mode B にすれば 1 TiB あたり毎月 $512 の取り込み料金がまるごと消えます。 Mode A 単独だと「保管の何割か」のオーダーで、ドル単位の話。 つまり「請求書を本気で減らしたいなら Mode B、削れるだけ削りたいなら両方」です。
ロックインしません ─── データは普通の zstd
S4 Logs に「依存」しないことを v1.0 で契約として固定しました (DESIGN.md §14)。
- 書き出すのは標準 RFC 8878 の zstd フレーム。 独自コンテナは使っていません
- 中身は 1 行 1 イベントの JSONL:
{"timestamp":…,"stream":"…","message":"…","ingestion_time":…,"event_id":"…"} - サイドカー (
.s4index/.s4lts) は range-read 索引なので、無視しても本体は読める - S4 Logs が消えても
aws s3 cp … - | zstd -dcで読める
Athena もそのまま使えます (docs/athena.md 同梱)。
実 AWS での検証
LocalStack だけで終わらせると怒られる気がしたので、本物の us-east-1 で計測しました (2026-06-10 / 06-12)。
- Mode A: 5.00 GiB / 33,163,647 イベントを seed → drain 94.6 分、
ThrottlingException0、 アーカイブ 1.6 GiB zstd (圧縮率 6.2×)、 Athenacount(*)で 33,163,613 件一致 (誤差 0.0001% は CloudWatch 側の seeder 残差で別件) - Mode B: ゲートウェイ越しの
PutLogEventsが実 S3 に正しいレイアウト (dt=…) で着地、 ルーティング (s3/cloudwatch/both) が分離していること、restore --to-log-groupが 14 日制約付きで動くこと、 SIGTERM でバッファをドレインすることを確認 - 2 時間ソーク: 715,817 req / 7,158,170 events ack / 0 失敗、 RSS 増分 2.3 MiB (リーク無し)
実験総コスト ~$2.60。 結果は README に表で出しています。
入れる
Linux x86_64 / aarch64 musl 静的バイナリ:
curl -fsSL https://raw.githubusercontent.com/abyo-software/s4-logs/main/scripts/install.sh | sh
Docker でも動きます (docker build -t s4logs . で ~176 MB のランタイムイメージ)。
v1.1.0 + v1.1.1 で変わったところ
v1.1.0 (2026-06-15): 共有レイヤ抽出 + Gateway TLS
S4 Logs を作っている過程で「S3 オブジェクトストア層」「観測性 (/health//ready//metrics) 層」「TLS 層」「EMF パーサ」は S4 ファミリの次の製品 (S4 Metrics) でもまるごと使えると判明したので、 v1.1.0 では 4 つの crate に切り出しました。
-
s4-objstore─── ペイロード非依存の S3 ObjectStore (CRC32C-on-PUT、 range GET、 paginated list) -
s4-observability─── Prometheus + readiness probe + graceful shutdown -
s4-tls─── rustls による TLS 終端 (PEM と ACME 両対応) -
s4-emf─── CloudWatch Embedded Metric Format のパーサ/シリアライザ
加えて Gateway に TLS オプション (PEM パス) が入りました:
s4logs serve --tls-cert /etc/s4logs/cert.pem --tls-key /etc/s4logs/key.pem ...
v1.1.1 (2026-06-27): ACME (Let's Encrypt) CLI + EMF DoS 防御
v1.1.0 で s4-tls の中には ACME (Let's Encrypt) 経路を実装していたものの、 CLI フラグまでは公開できていませんでした。 v1.1.1 で公開します:
s4logs serve --listen 0.0.0.0:443 \
--bucket my-archive-bucket --prefix s4logs --account 123456789012 \
--acme-domain logs.example.com \
--acme-contact ops@example.com \
--acme-cache-dir /var/lib/s4logs/acme
TLS-ALPN-01 チャレンジを使うので、 同一ポート (通常 :443) で証明書取得と通常トラフィックを共有します。 --acme-staging で Let's Encrypt staging 環境に切り替え可能 (rate limit 緩和されているので構成確認に便利)。 --tls-cert/--tls-key (PEM 静的) との同時指定は usage error で reject。
もう 1 つの追加が s4-emf::EmfDocument::samples_bounded():
let doc = s4_emf::parse(json_bytes)?;
let samples = doc.samples_bounded(max_samples, max_values)
.map_err(|e| /* 拒否 */)?;
EMF ドキュメントは flattening 段で爆発します。 長さ 1 万の values 配列を 1 万通りの dimension set に展開すれば、 百 KB 台の入力が約 800 MB (= 約 763 MiB) のアロケーションになります (1 万 × 1 万 × f64 8 byte = 8 × 10^8 byte)。 samples_bounded() は materialize する前にサイズ計算して、 cap を超える時点で EmfError::ExpansionTooLarge を返します。 untrusted な EMF を食わせる経路 (バイパス Gateway や S4 Metrics) で使ってください。 正常な document に対しては samples() と完全に同じ Vec<EmfSample> を返します。
on-disk フォーマットは 1.x で凍結したので、 v1.0 で書いたデータは v1.1.x で全部読めます。
v1.1.0 + v1.1.1 の設計判断 (なぜ 4 crate なのか、 なぜ ACME を 1.1.1 に分けたか、 expansion DoS をどう pre-materialization で弾くか) は、 Zenn の方に書いた深掘り記事に分けてあります
ライセンスと連絡先
- ライセンス: Apache-2.0
- 販売元: abyo software 合同会社
- サポート連絡先: aws-support@abyo.net
- issue / PR: https://github.com/abyo-software/s4-logs
商用版 (LRU リテンション、 マルチアカウントなど) も別途用意していますが、 まずは OSS 版で十分なケースが大半です ─── 月 $500 未満なら OSS で打ち止めにしてしまって構いません。 README に「いつ商用版を考えるべきか」の break-even ラインも書いてあります。
質問・要望は GitHub issue に投げてください。