(tests): webhook runtime (doc): logger options
This commit is contained in:
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.0
|
||||
|
||||
### Changed
|
||||
- Bot-level middleware blocks now emit a final `UpdateHandledEvent` with `Handled=false`, keeping observer update lifecycles balanced.
|
||||
- `BotOpts`, `tgapi.APIOpts`, and logger utility godoc now document `LOG_FORMAT`, `LogFormat`, and logger formatting options consistently.
|
||||
|
||||
### Tests
|
||||
- Added regression coverage proving bot-level middleware blocks still complete the observer update lifecycle.
|
||||
- Added webhook runtime regression coverage for request enqueue through worker execution of a command handler.
|
||||
|
||||
## v1.0.0-rc.16
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
+7
-2
@@ -71,7 +71,9 @@ type BotOpts struct {
|
||||
// It is zero when the options were not loaded from a versioned file.
|
||||
FileConfigVersion int
|
||||
|
||||
LogFormat utils.LogFormat
|
||||
// LogFormat selects text or JSON output for bot-managed loggers.
|
||||
LogFormat utils.LogFormat
|
||||
// LogFormatter customizes bot-managed logger writers when supported.
|
||||
LogFormatter *sneklog.Formatter
|
||||
}
|
||||
|
||||
@@ -92,7 +94,7 @@ type BotOpts struct {
|
||||
// - DROP_RL_OVERFLOW: "true" to drop updates on rate limit overflow
|
||||
// - STRICT_PAYLOAD_TYPE: "true" to reject callback payloads encoded in a different format
|
||||
// - MAX_WORKERS: maximum number of concurrent update handlers (default: 32)
|
||||
// - JSON_LOG:
|
||||
// - LOG_FORMAT: logger output format, "text" or "json" (default: "text")
|
||||
//
|
||||
// Returns a populated BotOpts.
|
||||
// NewBot validates required fields and returns ErrTokenRequired when TG_TOKEN is missing.
|
||||
@@ -254,10 +256,13 @@ func (opts *BotOpts) SetMaxWorkers(workers int) *BotOpts {
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetLogFormat sets the output format used by bot-managed loggers.
|
||||
func (opts *BotOpts) SetLogFormat(format utils.LogFormat) *BotOpts {
|
||||
opts.LogFormat = format
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetLogFormatter sets the formatter used by bot-managed logger writers.
|
||||
func (opts *BotOpts) SetLogFormatter(formatter *sneklog.Formatter) *BotOpts {
|
||||
opts.LogFormatter = formatter
|
||||
return opts
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||
"git.scuroneko.dev/scuroneko/sneklog/v2"
|
||||
@@ -108,6 +109,55 @@ func TestRunWebhookRuntimeExecutesRunners(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWebhookRuntimeProcessesEnqueuedUpdate(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
plugin := NewPlugin[NoData]("demo")
|
||||
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
|
||||
calls.Add(1)
|
||||
return nil
|
||||
}, "start")
|
||||
|
||||
bot := &Bot[NoData]{
|
||||
logger: sneklog.NewLogger(),
|
||||
webHookLogger: sneklog.NewLogger(),
|
||||
prefixes: []string{"/"},
|
||||
plugins: []Plugin[NoData]{*plugin},
|
||||
updateQueue: make(chan *tgapi.Update, 1),
|
||||
maxWorkers: 1,
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = bot.logger.Close()
|
||||
_ = bot.webHookLogger.Close()
|
||||
})
|
||||
|
||||
err := bot.runWebhookRuntime(context.Background(), func(ctx context.Context) error {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"update_id":9,"message":{"message_id":1,"date":1,"chat":{"id":1,"type":"private"},"from":{"id":2,"is_bot":false,"first_name":"Test"},"text":"/start"}}`))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
updateHandler(ctx, bot, "").ServeHTTP(rec, req)
|
||||
if rec.Result().StatusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status: got %d want %d", rec.Result().StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
deadline := time.After(time.Second)
|
||||
for calls.Load() == 0 {
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("webhook runtime did not process enqueued update")
|
||||
default:
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("runWebhookRuntime returned error: %v", err)
|
||||
}
|
||||
if calls.Load() != 1 {
|
||||
t.Fatalf("expected command handler to run once, got %d", calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookAllowedUpdatesUsesBotUpdateTypesByDefault(t *testing.T) {
|
||||
bot := &Bot[NoData]{
|
||||
updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage, tgapi.UpdateTypeCallbackQuery},
|
||||
|
||||
@@ -46,6 +46,14 @@ func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
|
||||
|
||||
for _, middleware := range bot.middlewares {
|
||||
if !middleware.Execute(msgCtx, bot.appData) {
|
||||
bot.safeEmitEvent(ctx, UpdateHandledEvent{
|
||||
UpdateID: u.UpdateID,
|
||||
UpdateType: u.Type,
|
||||
FromID: msgCtx.FromID,
|
||||
ChatID: msgCtx.ChatID,
|
||||
Duration: time.Since(startTime),
|
||||
Handled: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
+42
-1
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type recordingObserver struct {
|
||||
received []UpdateReceivedEvent
|
||||
started []HandlerStartedEvent
|
||||
finished []HandlerFinishedEvent
|
||||
errors []ErrorEvent
|
||||
@@ -19,7 +20,9 @@ type recordingObserver struct {
|
||||
retries []PollingRetryEvent
|
||||
}
|
||||
|
||||
func (*recordingObserver) OnReceiveUpdate(context.Context, UpdateReceivedEvent) {}
|
||||
func (o *recordingObserver) OnReceiveUpdate(_ context.Context, ev UpdateReceivedEvent) {
|
||||
o.received = append(o.received, ev)
|
||||
}
|
||||
func (o *recordingObserver) OnHandledUpdate(_ context.Context, ev UpdateHandledEvent) {
|
||||
o.handled = append(o.handled, ev)
|
||||
}
|
||||
@@ -587,6 +590,44 @@ func TestHandleUpdateObserverEmitsUpdateErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleObserverCompletesUpdateWhenBotMiddlewareBlocks(t *testing.T) {
|
||||
observer := &recordingObserver{}
|
||||
bot := &Bot[NoData]{
|
||||
logger: sneklog.NewLogger(),
|
||||
observer: observer,
|
||||
middlewares: []Middleware[NoData]{
|
||||
NewMiddleware("block", func(ctx *MsgContext, db NoData) bool {
|
||||
return false
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
bot.handle(context.Background(), &tgapi.Update{
|
||||
UpdateID: 8,
|
||||
Type: tgapi.UpdateTypeMessage,
|
||||
Message: &tgapi.Message{
|
||||
MessageID: 1,
|
||||
Date: 1,
|
||||
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
|
||||
Text: "/start",
|
||||
},
|
||||
})
|
||||
|
||||
if len(observer.received) != 1 {
|
||||
t.Fatalf("expected one received event, got %d", len(observer.received))
|
||||
}
|
||||
if len(observer.handled) != 1 {
|
||||
t.Fatalf("expected one handled event, got %d", len(observer.handled))
|
||||
}
|
||||
got := observer.handled[0]
|
||||
if got.UpdateID != 8 || got.UpdateType != tgapi.UpdateTypeMessage || got.ChatID != 42 || got.Handled {
|
||||
t.Fatalf("unexpected handled event: %#v", got)
|
||||
}
|
||||
if len(observer.started) != 0 || len(observer.finished) != 0 || len(observer.errors) != 0 {
|
||||
t.Fatalf("middleware block should not emit handler lifecycle or errors: started=%d finished=%d errors=%d", len(observer.started), len(observer.finished), len(observer.errors))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMessageFallbackRunsAfterCommandMiss(t *testing.T) {
|
||||
observer := &recordingObserver{}
|
||||
called := false
|
||||
|
||||
@@ -64,10 +64,13 @@ func (opts *APIOpts) SetAPIURL(apiURL string) *APIOpts {
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetLogFormat sets the output format used by API-managed loggers.
|
||||
func (opts *APIOpts) SetLogFormat(format utils.LogFormat) *APIOpts {
|
||||
opts.logFormat = format
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetLogFormatter sets the formatter used by API-managed logger writers.
|
||||
func (opts *APIOpts) SetLogFormatter(formatter *sneklog.Formatter) *APIOpts {
|
||||
opts.logFormatter = formatter
|
||||
return opts
|
||||
|
||||
+5
-2
@@ -6,10 +6,13 @@ import (
|
||||
"git.scuroneko.dev/scuroneko/sneklog/v2"
|
||||
)
|
||||
|
||||
// LogFormat selects the writer format used by framework loggers.
|
||||
type LogFormat string
|
||||
|
||||
const (
|
||||
// LogFormatText writes human-readable text logs.
|
||||
LogFormatText LogFormat = "text"
|
||||
// LogFormatJSON writes structured JSON logs.
|
||||
LogFormatJSON LogFormat = "json"
|
||||
)
|
||||
|
||||
@@ -22,8 +25,8 @@ func GetLoggerLevel() sneklog.LogLevel {
|
||||
return level
|
||||
}
|
||||
|
||||
// CreateLogger creates a logger with the shared default policy:
|
||||
// JSON stdout output, provided prefix, and provided level.
|
||||
// CreateLogger creates a logger with stdout output, the provided name, level,
|
||||
// format, and optional formatter.
|
||||
func CreateLogger(
|
||||
name string, level sneklog.LogLevel,
|
||||
format LogFormat, formatter *sneklog.Formatter,
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ package utils
|
||||
|
||||
const (
|
||||
// VersionString is the module version string.
|
||||
VersionString = "1.0.0-rc.16"
|
||||
VersionString = "1.0.0"
|
||||
// VersionMajor is the module major version.
|
||||
VersionMajor = 1
|
||||
// VersionMinor is the module minor version.
|
||||
@@ -10,5 +10,5 @@ const (
|
||||
// VersionPatch is the module patch version.
|
||||
VersionPatch = 0
|
||||
// VersionBeta is the prerelease counter for the current version.
|
||||
VersionBeta = 16
|
||||
VersionBeta = 0
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user