feat(observability): request log middleware + in-memory metrics

Phase 11 partial of the go-port-cloud-run plan. Code-side
observability hooks ready ahead of Phase 01 GCP rollout.

- internal/server/log_middleware.go: HTTP middleware that wraps the
  router and emits {msg:"req", method, path, status, ms} per
  request. statusRecorder defaults to 200 when the inner handler
  doesn't call WriteHeader (Go writes 200 implicitly on first body
  write). Wired into server.New so /, /webhook, /cron/* all log.
- internal/metrics/counters.go: in-memory Registry with
  IncCommand/IncError/IncAI. Atomic Int64 per name + RWMutex on the
  map; steady-state increments are mutex-free. Periodic Run flushes
  via the project logger every 60s and one final flush on ctx done.
  Empty flush is silent (no-noise default).
- Dispatcher instrumented: every command invocation calls
  metrics.IncCommand; every handler error calls
  metrics.IncError("handler-error"). Logger keeps the full error
  detail; counters keep the rate.
- cmd/server/main.go: go metrics.Run(rootCtx) so the flush loop
  cancels with SIGTERM and emits the trailing window before exit.

Test coverage: 12 new tests (7 metrics, 3 middleware, 2 default-
registry round-trip). go test -race -count=1 ./... clean
(20 packages); golangci-lint clean.

Soak / cold-start measurement / log-based metrics setup deferred to
post-deployment (Phase 01 prerequisite).
This commit is contained in:
tiennm99 committed 2026-05-09 17:30:15 +07:00
1 parent 4bcd8ed7cf
commit 2b9bbf27ae
9 files changed
+494 -10

No files matched your search

+3
View File
@@ -7,6 +7,7 @@ import (
"github.com/go-telegram/bot/models"
"github.com/tiennm99/miti99bot-go/internal/log"
"github.com/tiennm99/miti99bot-go/internal/metrics"
)
// Auth gates Protected/Private commands by sender Telegram user ID. Public
@@ -59,7 +60,9 @@ func Install(b *bot.Bot, reg *Registry, auth Auth) {
if !auth.Permits(cmdCopy.Visibility, update) {
return // silent — do not leak existence of gated commands
}
metrics.IncCommand(cmdCopy.Name)
if err := cmdCopy.Handler(ctx, b, update); err != nil {
metrics.IncError("handler-error")
log.Error("command failed", "command", cmdCopy.Name, "err", err)
}
},