mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-09-16 12:21:51 +00:00
Rename: - Go module github.com/tiennm99/miti99bot-go → github.com/tiennm99/miti99bot - CloudFormation stack miti99bot-aws-port → miti99bot - Drop "port", "Cloud Run", "GCP", "cutover", "Phase NN" framing from active code and docs — project reads as canonical AWS-Lambda from now on. AWS deploy guide + flow fix: - New docs/deploy-aws-free-tier-guide.md — Ubuntu 24.04 ARM64 onboarding with project-local venv (pip awscli + sam-cli), SSM secrets via read -s, idempotent OIDC provider + role creation, $1 budget alarm. - Drop sam build from the pipeline — provided.al2023 + makefile builder expects a Makefile in CodeUri (build/lambda/, the output dir), so the step always fails. sam deploy --template-file template.yaml now reads the raw template and zips build/lambda/ directly. - Rollback section rewritten — use continue-update-rollback / cancel-update-stack / git-SHA redeploy. Drop the broken --use-previous-template recipe. - DynamoDB free-tier row corrected (on-demand is 2.5M read / 1M write request units, not 25 RCU/WCU). Updated: - README.md fully rewritten (drops port/legacy framing, lists modules, points new users at the free-tier guide). - aws/README.md retitled "AWS account setup", phase numbers stripped. - Makefile / .github/workflows/deploy.yml — sam deploy flow. - samconfig.toml — stack_name = "miti99bot". - Go comments — Cloud Run → Lambda, Cloud Scheduler → EventBridge Scheduler, Cloud Logging → CloudWatch Logs. - Struct field GCPProject → FirestoreProject (env GOOGLE_CLOUD_PROJECT unchanged). Plus advisory reports under plans/reports/ from the code-reviewer + researcher passes that informed the fixes. Verified: go vet ./..., go build ./..., go test ./... all green.
137 lines
3.4 KiB
Go
137 lines
3.4 KiB
Go
package metrics
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
logger "github.com/tiennm99/miti99bot/internal/log"
|
|
)
|
|
|
|
// captureLogger swaps the package-level logger for one writing to buf and
|
|
// returns a restore func. Tests must defer the restore.
|
|
func captureLogger(t *testing.T) (*bytes.Buffer, func()) {
|
|
t.Helper()
|
|
prev := logger.Default()
|
|
buf := &bytes.Buffer{}
|
|
logger.SetDefault(slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelInfo})))
|
|
return buf, func() { logger.SetDefault(prev) }
|
|
}
|
|
|
|
func TestRegistry_IncCommand_AccumulatesCounts(t *testing.T) {
|
|
r := New()
|
|
r.IncCommand("wordle")
|
|
r.IncCommand("wordle")
|
|
r.IncCommand("loldle")
|
|
|
|
cmds, _, _ := r.snapshot()
|
|
if cmds["wordle"] != 2 {
|
|
t.Errorf("wordle = %d, want 2", cmds["wordle"])
|
|
}
|
|
if cmds["loldle"] != 1 {
|
|
t.Errorf("loldle = %d, want 1", cmds["loldle"])
|
|
}
|
|
}
|
|
|
|
func TestRegistry_Snapshot_ResetsCounters(t *testing.T) {
|
|
r := New()
|
|
r.IncCommand("wordle")
|
|
r.snapshot() // first snapshot drains
|
|
cmds, _, _ := r.snapshot()
|
|
if len(cmds) != 0 {
|
|
t.Errorf("after drain, snapshot = %v, want empty", cmds)
|
|
}
|
|
}
|
|
|
|
func TestRegistry_Flush_EmitsMetricsLine(t *testing.T) {
|
|
buf, restore := captureLogger(t)
|
|
defer restore()
|
|
|
|
r := New()
|
|
r.IncCommand("wordle")
|
|
r.IncError("ai-429")
|
|
r.Flush()
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, `"msg":"metrics"`) {
|
|
t.Errorf("flush output missing msg=metrics: %s", output)
|
|
}
|
|
if !strings.Contains(output, `"wordle":1`) {
|
|
t.Errorf("flush output missing wordle counter: %s", output)
|
|
}
|
|
if !strings.Contains(output, `"ai-429":1`) {
|
|
t.Errorf("flush output missing error counter: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestRegistry_Flush_EmptyIsSilent(t *testing.T) {
|
|
buf, restore := captureLogger(t)
|
|
defer restore()
|
|
r := New()
|
|
r.Flush()
|
|
if buf.Len() != 0 {
|
|
t.Errorf("empty flush should produce no output; got %q", buf.String())
|
|
}
|
|
}
|
|
|
|
// Steady-state increments should not race under -race. Hammer with
|
|
// goroutines and verify the total adds up.
|
|
func TestRegistry_ConcurrentInc(t *testing.T) {
|
|
r := New()
|
|
const goroutines = 16
|
|
const itersEach = 1000
|
|
var wg sync.WaitGroup
|
|
wg.Add(goroutines)
|
|
for i := 0; i < goroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < itersEach; j++ {
|
|
r.IncCommand("hot")
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
cmds, _, _ := r.snapshot()
|
|
if got := cmds["hot"]; got != goroutines*itersEach {
|
|
t.Errorf("hot = %d, want %d", got, goroutines*itersEach)
|
|
}
|
|
}
|
|
|
|
func TestPackageDefault_RoundTrip(t *testing.T) {
|
|
// The package-level default is shared global state. Snapshot first to
|
|
// clear any leakage from earlier tests in the same binary.
|
|
Default.snapshot()
|
|
|
|
IncCommand("ping")
|
|
IncError("kv-fail")
|
|
IncAI("flash")
|
|
|
|
cmds, errs, ai := Default.snapshot()
|
|
if cmds["ping"] != 1 || errs["kv-fail"] != 1 || ai["flash"] != 1 {
|
|
t.Errorf("default registry: cmds=%v errs=%v ai=%v", cmds, errs, ai)
|
|
}
|
|
}
|
|
|
|
// Sanity check that the metrics line is valid JSON, not just a prefix.
|
|
func TestRegistry_Flush_OutputIsValidJSON(t *testing.T) {
|
|
buf, restore := captureLogger(t)
|
|
defer restore()
|
|
r := New()
|
|
r.IncCommand("wordle")
|
|
r.Flush()
|
|
|
|
// One JSON line per slog record.
|
|
for _, line := range strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var rec map[string]any
|
|
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
|
t.Errorf("not JSON: %q (%v)", line, err)
|
|
}
|
|
}
|
|
}
|