この記事は、Wano Group Advent Calendar 2025の3日目の記事です。
2日目は、@rom1000_onigiri さんの、【MCP戦国時代】なるべく怪我を負わず生き抜く着眼点をざっくりと鍛えるでした。
外部APIのエラーと監視
外部APIを叩いているんですが、たまに調子が悪い時があって、500エラーとかタイムアウトをしてしまうんですが、そのときに、自分たちのアプリケーションで、Errorのログを出すと、監視に引っかかって鬱陶しい...。
ただ、しょっちゅう上がってるようなら、提供先に連絡した方が良い。
という悩みがありまして。まともな監視があれば、Errorの回数とかでアラートを飛ばすこともできるのでしょうが、現状、キーワードマッチで飛ばしているだけなので、どうも困ったなという話です。
どうしたか
かなり苦肉の策っぽいのですが、一定回数を超えるとError、それ以下であればWarnという感じにしました。
- 一定期間以内に
- 一定数以上呼ばれたら
- Warnではなくて、Errorにする
- ただし、一回Errorになったら、しばらくWarnに戻る
という感じです。
作ったもの&使い方
PeriodCounterというものを作りました。
使い方は、まず、以下のようにグローバルに定義します。
const apiPeriodCounterThreshold = 10
const apiPeriodCounterResetSecond = 3600 // 1時間
var pc = entity.NewPeriodicCounter(time.Now(), time.Second*apiPeriodCounterResetSecond, apiPeriodCounterThreshold)
後は、ログを出したいタイミングで、以下のようにすれば、一定期間内に一定数以上起きているなら、Errorになります。
err := callHogeHogeApi()
if err != nil {
pc.Log("hogehoge %v", err)
}
という単純な感じです。
実装
type PeriodicCounter struct {
// カウンタ
count int
// リセットするまでの間隔
resetDuration time.Duration
// リセットする日時
resetTime time.Time
// Errorにする閾値
alertCount int
}
func NewPeriodicCounter(t time.Time, resetDuration time.Duration, alertCount int) *PeriodicCounter {
counter := &PeriodicCounter{
count: 0,
resetDuration: resetDuration,
resetTime: t,
alertCount: alertCount,
}
counter.reset(t)
return counter
}
func (t *PeriodicCounter) reset(now time.Time) {
t.count = 0
t.resetTime = now.Add(t.resetDuration)
}
func (t *PeriodicCounter) Increment(now time.Time) int {
// 時間が過ぎていたらリセットする
if t.resetTime.Before(now) {
t.reset(now)
}
// カウントを増やす
t.count++
return t.count
}
func (t *PeriodicCounter) Log(now time.Time, msg string, args ...any) {
t.Increment(now)
// Errorにすべきタイミングか判定
if t.ShouldAlert() {
log.Error(t.Message(msg, args...))
} else {
log.Warn(t.Message(msg, args...))
}
}
func (t *PeriodicCounter) ShouldAlert() bool {
if t.count%t.alertCount != 0 {
return false
}
// 閾値で割る
n := t.count / t.alertCount
// 設定したアラート閾値の累乗回目の場合のみアラートする
return t.count >= t.alertCount && n&(n-1) == 0
}
func (t *PeriodicCounter) Message(msg string, args ...any) string {
return fmt.Sprintf("[%d times] %s", t.count, fmt.Sprintf(msg, args[0:]))
}
おまけ: 2の累乗判定
累乗の判定n&(n-1) == 0 ですが、n(n > 0)が「2の累乗の数」とすると、以下のようになります。
n を2進数で表すと
| n(10進数) | n(2進数) |
|---|---|
| 1 | 1 |
| 2 | 10 |
| 4 | 100 |
| 8 | 1000 |
| 16 | 10000 |
となります。最上位ビットに1があり、下位ビットはすべて0になります。
一方 nが2の累乗のときのn-1は、
| n(10進数) | n(2進数) |
|---|---|
| 0 | 0 |
| 1 | 01 |
| 3 | 011 |
| 7 | 0111 |
| 15 | 01111 |
※2の累乗のnと桁を合わせています
となり、2の累乗と桁を合わせると、最上位ビットは0で、残りすべてのビットが1になります(0を除く)。もうおわかりと思いますが、n&(n-1)を表に表すと、
| n(10進数) | n(2進数) | n-1 | n&(n-1) |
|---|---|---|---|
| 1 | 1 | 0 | 0 |
| 2 | 10 | 01 | 0 |
| 4 | 100 | 011 | 0 |
| 8 | 1000 | 0111 | 0 |
| 16 | 10000 | 01111 | 0 |
のように、ビットアンドすることで、0になると2の累乗と判定できます。
おまけ: Popcount (Brian Kernighanのアルゴリズム)
nが2の累乗でなければ、以下のようになり、0にはなりませんが、
| n(10進数) | n(2進数) | n-1 | n&(n-1) |
|---|---|---|---|
| 3 | 11 | 10 | 10 |
| 5 | 101 | 100 | 100 |
| 6 | 110 | 101 | 100 |
| 7 | 111 | 110 | 110 |
| 9 | 1001 | 1000 | 1000 |
この、n&(n-1)は、「nの一番下位にある1のビット(2の累乗の場合、最上位ビット)を0にする」という処理になっています。
これを、n = 0になるまで繰り返すと、ある数値を2進数で表したときに1の立っているビットがいくつあるのかを数えることができます。
終わり
というわけで、ログ監視におけるちょっとした工夫というか苦肉の策というお題でした。参考になれば幸いです。
より良い監視システムを構築して、お役御免になれば良いなと思っています。
明日は、@NaotoFushimi さんの記事になります。お楽しみに。