7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ログ監視におけるちょっとした工夫というか苦肉の策

Last updated at Posted at 2025-12-03

この記事は、Wano Group Advent Calendar 2025の3日目の記事です。
2日目は、@rom1000_onigiri さんの、【MCP戦国時代】なるべく怪我を負わず生き抜く着眼点をざっくりと鍛えるでした。

外部APIのエラーと監視

外部APIを叩いているんですが、たまに調子が悪い時があって、500エラーとかタイムアウトをしてしまうんですが、そのときに、自分たちのアプリケーションで、Errorのログを出すと、監視に引っかかって鬱陶しい...。
ただ、しょっちゅう上がってるようなら、提供先に連絡した方が良い。

という悩みがありまして。まともな監視があれば、Errorの回数とかでアラートを飛ばすこともできるのでしょうが、現状、キーワードマッチで飛ばしているだけなので、どうも困ったなという話です。

どうしたか

かなり苦肉の策っぽいのですが、一定回数を超えるとError、それ以下であればWarnという感じにしました。

  1. 一定期間以内に
  2. 一定数以上呼ばれたら
  3. 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 さんの記事になります。お楽しみに。

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?