mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-09-13 08:19:39 +00:00
Concurrency - lolschedule: serialize subscriber Get→mutate→Put via state.subscribersMu; the single-slot list was previously losing writes under concurrent /lolschedule_subscribe. - trading: PriceClient memoises its default *http.Client so /trade_stats reuses TLS connections across held tickers. Observability - server/log_middleware: defer the req log line and recover panics so a panicking cron handler still emits the structured req entry CloudWatch filters on for 5xx alerting. - server/router (cron): inner recover with cron-name context captures the panicking job before the middleware's safety net does. - telegram/webhook: rune-safe truncation in dispatch logs — Vietnamese, Korean, and emoji previews no longer ship as garbled bytes. - lolschedule/api_client: same rune-safe fix for error-body log truncation. - telegram/webhook: gate the post-recover WriteHeader(200) so a panicking handler that already touched w doesn't trigger superfluous-WriteHeader. Correctness - twentyq: clearGame error during solved-relaunch is logged instead of silently swallowed (was a permanent deadlock vector on KV failure). - misc /mstats: KV read failure replies "Could not load stats. Try again later." to the user instead of returning into the dispatcher; matches the pattern other modules use. - migrate_cf_data trading-audit-dump: surface f.Close error so a truncated JSONL never passes silently as a complete audit dump. Operator ergonomics - migrate_cf_data (all 4 subcommands): signal.NotifyContext for SIGINT / SIGTERM. Ctrl-C mid-Scan now propagates cleanly instead of leaving a half-converted DynamoDB table. - ai/ratelimit: doc the Lambda-recycle memory bound to match keylock.Map so a future reviewer doesn't re-flag the unbounded map. I/O-changing (user-approved) - lolschedule daily push auto-prunes subscribers whose Telegram error matches a terminal marker (blocked / deactivated / chat gone). Transient errors keep the chat on the list. Subscribe message updated to mention the auto-cleanup. - twentyq seed pool grown 50 → 178; repeat-collision threshold moves from ~9 plays to ~17 (birthday paradox). - util /info flipped Public → Protected — chat/thread/sender IDs are no longer enumerable by every group member. - cmd/server WriteTimeout 6min → 75s (cron 60s + 15s slack). No-op on Lambda; matters only for local non-Lambda runs. - webhook + cron rejection paths drop response bodies (no fingerprintable text for internet scanners hitting the public Function URL). Status codes preserved for CloudWatch metrics; structured log lines carry the rejection reason for operator triage. Tests added: TestTruncateRunes, TestRunDailyPush_PrunesDeadSubscribers, TestIsTerminalSendError, TestInfo_DeniedToNonOwner, TestInfo_DeniedToChannelMessageNoFrom, plus owner-allowed counterparts.
79 lines
2.4 KiB
Go
79 lines
2.4 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"runtime/debug"
|
|
"time"
|
|
|
|
"github.com/tiennm99/miti99bot/internal/log"
|
|
)
|
|
|
|
// statusRecorder wraps http.ResponseWriter to capture the final status
|
|
// code. http.ResponseWriter doesn't expose what was written; the middleware
|
|
// needs the status to log a per-request `req` line.
|
|
type statusRecorder struct {
|
|
http.ResponseWriter
|
|
status int
|
|
}
|
|
|
|
func (r *statusRecorder) WriteHeader(code int) {
|
|
r.status = code
|
|
r.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
// status returns the recorded status code, defaulting to 200 when no
|
|
// explicit WriteHeader was called (Go's net/http implicitly writes 200 on
|
|
// the first body write).
|
|
func (r *statusRecorder) effectiveStatus() int {
|
|
if r.status == 0 {
|
|
return http.StatusOK
|
|
}
|
|
return r.status
|
|
}
|
|
|
|
// LogRequests wraps an http.Handler with a request log line:
|
|
//
|
|
// {"msg":"req","method":"POST","path":"/webhook","status":200,"ms":12}
|
|
//
|
|
// CloudWatch Logs filters on `jsonPayload.msg=req AND jsonPayload.status>=500`
|
|
// for 5xx-rate alerting. Mirrors the JS source's index.js shape.
|
|
//
|
|
// The req line is emitted from a deferred closure so a panic in a downstream
|
|
// handler still produces an observable log entry — without this, a cron
|
|
// panic would disappear silently (http.Server does its own recover but never
|
|
// runs middleware again on the way out).
|
|
func LogRequests(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
rec := &statusRecorder{ResponseWriter: w}
|
|
defer func() {
|
|
rec.status = recoverPanicStatus(recover(), rec.status)
|
|
log.Info("req",
|
|
"method", r.Method,
|
|
"path", r.URL.Path,
|
|
"status", rec.effectiveStatus(),
|
|
"ms", time.Since(start).Milliseconds(),
|
|
)
|
|
}()
|
|
next.ServeHTTP(rec, r)
|
|
})
|
|
}
|
|
|
|
// recoverPanicStatus folds a recovered panic into the status to log: returns
|
|
// 500 if a panic was recovered (and re-panics nothing — http.Server will
|
|
// terminate the connection cleanly while the deferred req log still runs),
|
|
// otherwise returns the original status untouched.
|
|
//
|
|
// Re-panicking would lose the deferred log line in some recover-order edge
|
|
// cases; absorbing the panic here matches the webhook handler's posture of
|
|
// "log the failure, keep the goroutine clean".
|
|
func recoverPanicStatus(rec any, currentStatus int) int {
|
|
if rec == nil {
|
|
return currentStatus
|
|
}
|
|
log.Error("middleware recovered panic",
|
|
"panic", rec,
|
|
"stack", string(debug.Stack()))
|
|
return http.StatusInternalServerError
|
|
}
|