feat: update kilovolt, replace zap with slog

This commit is contained in:
Ash Keel 2024-03-14 13:33:52 +01:00
parent a06b9457ea
commit f4930d7758
No known key found for this signature in database
GPG Key ID: 53A9E9A6035DD109
39 changed files with 382 additions and 426 deletions

64
app.go
View File

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"mime/multipart"
"net/http"
"os"
@ -13,14 +14,7 @@ import (
"strconv"
"git.sr.ht/~ashkeel/containers/sync"
"github.com/nicklaw5/helix/v2"
kv "github.com/strimertul/kilovolt/v11"
"github.com/urfave/cli/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
kv "git.sr.ht/~ashkeel/kilovolt/v12"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/docs"
"git.sr.ht/~ashkeel/strimertul/loyalty"
@ -28,6 +22,10 @@ import (
"git.sr.ht/~ashkeel/strimertul/twitch"
"git.sr.ht/~ashkeel/strimertul/twitch/client"
"git.sr.ht/~ashkeel/strimertul/webserver"
"github.com/nicklaw5/helix/v2"
"github.com/urfave/cli/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App struct
@ -60,7 +58,6 @@ func (a *App) startup(ctx context.Context) {
defer func() {
if r := recover(); r != nil {
a.stop(ctx)
_ = logger.Sync()
switch v := r.(type) {
case error:
a.showFatalError(v, v.Error())
@ -70,7 +67,7 @@ func (a *App) startup(ctx context.Context) {
}
}()
logger.Info("Started", zap.String("version", appVersion))
slog.Info("Started", slog.String("version", appVersion))
a.ctx = ctx
@ -87,7 +84,7 @@ func (a *App) startup(ctx context.Context) {
}
// Check for migrations
if err := migrations.Run(a.driver, a.db, logger.With(zap.String("operation", "migration"))); err != nil {
if err := migrations.Run(a.driver, a.db, slog.With(slog.String("operation", "migration"))); err != nil {
a.showFatalError(err, "Failed to migrate database to latest version")
return
}
@ -103,7 +100,7 @@ func (a *App) startup(ctx context.Context) {
a.ready.Set(true)
runtime.EventsEmit(ctx, "ready", true)
logger.Info("Strimertul is ready")
slog.Info("Strimertul is ready")
// Add logs I/O to UI
a.cancelLogs, _ = a.listenForLogs()
@ -134,7 +131,7 @@ func (a *App) initializeDatabase() error {
go hub.Run()
hub.UseInteractiveAuth(a.interactiveAuth)
a.db, err = database.NewLocalClient(hub, logger)
a.db, err = database.NewLocalClient(hub)
if err != nil {
return fmt.Errorf("could not initialize database client: %w", err)
}
@ -146,19 +143,19 @@ func (a *App) initializeComponents() error {
var err error
// Create logger and endpoints
a.httpServer, err = webserver.NewServer(a.db, logger, webserver.DefaultServerFactory)
a.httpServer, err = webserver.NewServer(a.db, webserver.DefaultServerFactory)
if err != nil {
return fmt.Errorf("could not initialize http server: %w", err)
}
// Create twitch client
a.twitchManager, err = client.NewManager(a.ctx, a.db, a.httpServer, logger)
a.twitchManager, err = client.NewManager(a.ctx, a.db, a.httpServer)
if err != nil {
return fmt.Errorf("could not initialize twitch client: %w", err)
}
// Initialize loyalty system
a.loyaltyManager, err = loyalty.NewManager(a.db, logger)
a.loyaltyManager, err = loyalty.NewManager(a.db)
if err != nil {
return fmt.Errorf("could not initialize loyalty manager: %w", err)
}
@ -173,13 +170,12 @@ func (a *App) listenForLogs() (database.CancelFunc, error) {
return
}
level, err := zapcore.ParseLevel(string(entry.Level))
if err != nil {
level = zapcore.InfoLevel
var level slog.Level
if err := level.UnmarshalText([]byte(entry.Level)); err != nil {
level = slog.LevelInfo
}
fields := parseAsFields(entry.Data)
logger.Log(level, entry.Message, fields...)
slog.Log(a.ctx, level, entry.Message, parseLogFields(entry.Data)...)
})
}
@ -209,7 +205,7 @@ func (a *App) AuthenticateKVClient(id string) {
if err != nil {
return
}
warnOnError(a.driver.Hub().SetAuthenticated(idInt, true), "Could not mark session as authenticated", zap.String("session-id", id))
warnOnError(a.driver.Hub().SetAuthenticated(idInt, true), "Could not mark session as authenticated", slog.String("session-id", id))
}
func (a *App) IsServerReady() bool {
@ -249,27 +245,26 @@ func (a *App) SendCrashReport(errorData string, info string) (string, error) {
// Add text fields
if err := w.WriteField("error", errorData); err != nil {
logger.Error("Could not encode field error for crash report", zap.Error(err))
slog.Error("Could not encode field error for crash report", "error", err)
}
if len(info) > 0 {
if err := w.WriteField("info", info); err != nil {
logger.Error("Could not encode field info for crash report", zap.Error(err))
slog.Error("Could not encode field info for crash report", "error", err)
}
}
// Add log files
_ = logger.Sync()
addFile(w, "log", logFilename)
addFile(w, "paniclog", panicFilename)
if err := w.Close(); err != nil {
logger.Error("Could not prepare request for crash report", zap.Error(err))
slog.Error("Could not prepare request for crash report", "error", err)
return "", err
}
resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b)
if err != nil {
logger.Error("Could not send crash report", zap.Error(err))
slog.Error("Could not send crash report", "error", err)
return "", err
}
defer resp.Body.Close()
@ -277,7 +272,7 @@ func (a *App) SendCrashReport(errorData string, info string) (string, error) {
// Check the response
if resp.StatusCode != http.StatusOK {
byt, _ := io.ReadAll(resp.Body)
logger.Error("Crash report server returned error", zap.String("status", resp.Status), zap.String("response", string(byt)))
slog.Error("Crash report server returned error", slog.String("status", resp.Status), slog.String("response", string(byt)))
return "", fmt.Errorf("crash report server returned error: %s - %s", resp.Status, string(byt))
}
@ -326,11 +321,10 @@ func (a *App) interactiveAuth(client kv.Client, message map[string]any) bool {
return <-authResult
}
func (a *App) showFatalError(err error, text string, fields ...zap.Field) {
func (a *App) showFatalError(err error, text string, fields ...any) {
if err != nil {
fields = append(fields, zap.Error(err))
fields = append(fields, zap.String("Z", string(debug.Stack())))
logger.Error(text, fields...)
fields = append(fields, "error", err, slog.String("Z", string(debug.Stack())))
slog.Error(text, fields...)
runtime.EventsEmit(a.ctx, "fatalError")
a.isFatalError.Set(true)
}
@ -347,17 +341,17 @@ func (a *App) onSecondInstanceLaunch(_ options.SecondInstanceData) {
func addFile(m *multipart.Writer, field string, filename string) {
logfile, err := m.CreateFormFile(field, filename)
if err != nil {
logger.Error("Could not encode field log for crash report", zap.Error(err))
slog.Error("Could not encode field log for crash report", "error", err)
return
}
file, err := os.Open(filename)
if err != nil {
logger.Error("Could not open file for including in crash report", zap.Error(err), zap.String("file", filename))
slog.Error("Could not open file for including in crash report", slog.String("file", filename), "error", err)
return
}
if _, err = io.Copy(logfile, file); err != nil {
logger.Error("Could not read from file for including in crash report", zap.Error(err), zap.String("file", filename))
slog.Error("Could not read from file for including in crash report", slog.String("file", filename), "error", err)
}
}

View File

@ -2,28 +2,27 @@ package main
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"sort"
"time"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/utils"
)
func BackupTask(driver database.Driver, options database.BackupOptions) {
if options.BackupDir == "" {
logger.Warn("Backup directory not set, database backups are disabled")
slog.Warn("Backup directory not set, database backups are disabled")
return
}
err := os.MkdirAll(options.BackupDir, 0o755)
if err != nil {
logger.Error("Could not create backup directory, moving to a temporary folder", zap.Error(err))
slog.Error("Could not create backup directory, moving to a temporary folder", "error", err)
options.BackupDir = os.TempDir()
logger.Info("Using temporary directory", zap.String("backup-dir", options.BackupDir))
slog.Info("Using temporary directory", slog.String("backup-dir", options.BackupDir))
return
}
@ -38,21 +37,21 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
// Run backup procedure
file, err := os.Create(fmt.Sprintf("%s/%s.db", options.BackupDir, time.Now().Format("20060102-150405")))
if err != nil {
logger.Error("Could not create backup file", zap.Error(err))
slog.Error("Could not create backup file", "error", err)
return
}
err = driver.Backup(file)
if err != nil {
logger.Error("Could not backup database", zap.Error(err))
slog.Error("Could not backup database", "error", err)
}
_ = file.Close()
logger.Info("Database backup created", zap.String("backup-file", file.Name()))
slog.Info("Database backup created", slog.String("backup-file", file.Name()))
// Remove old backups
files, err := os.ReadDir(options.BackupDir)
if err != nil {
logger.Error("Could not read backup directory", zap.Error(err))
slog.Error("Could not read backup directory", "error", err)
return
}
@ -65,7 +64,7 @@ func performBackup(driver database.Driver, options database.BackupOptions) {
for _, file := range toRemove {
err = os.Remove(fmt.Sprintf("%s/%s", options.BackupDir, file.Name()))
if err != nil {
logger.Error("Could not remove backup file", zap.Error(err))
slog.Error("Could not remove backup file", "error", err)
}
}
}
@ -80,7 +79,7 @@ type BackupInfo struct {
func (a *App) GetBackups() (list []BackupInfo) {
files, err := os.ReadDir(a.backupOptions.BackupDir)
if err != nil {
logger.Error("Could not read backup directory", zap.Error(err))
slog.Error("Could not read backup directory", "error", err)
return nil
}
@ -91,7 +90,7 @@ func (a *App) GetBackups() (list []BackupInfo) {
info, err := file.Info()
if err != nil {
logger.Error("Could not get info for backup file", zap.Error(err))
slog.Error("Could not get info for backup file", "error", err)
continue
}
@ -111,7 +110,7 @@ func (a *App) RestoreBackup(backupName string) error {
if err != nil {
return fmt.Errorf("could not open import file for reading: %w", err)
}
defer utils.Close(file, logger)
defer utils.Close(file)
inStream := file
if a.driver == nil {
@ -126,6 +125,6 @@ func (a *App) RestoreBackup(backupName string) error {
return fmt.Errorf("could not restore database: %w", err)
}
logger.Info("Restored database from backup")
slog.Info("Restored database from backup")
return nil
}

View File

@ -1,13 +1,13 @@
package main
import (
"log/slog"
"os"
"git.sr.ht/~ashkeel/strimertul/utils"
"github.com/urfave/cli/v2"
"git.sr.ht/~ashkeel/strimertul/database"
"github.com/urfave/cli/v2"
"git.sr.ht/~ashkeel/strimertul/utils"
)
func cliImport(ctx *cli.Context) error {
@ -18,7 +18,7 @@ func cliImport(ctx *cli.Context) error {
if err != nil {
return fatalError(err, "could not open import file for reading")
}
defer utils.Close(file, logger)
defer utils.Close(file)
inStream = file
}
var entries map[string]string
@ -37,7 +37,7 @@ func cliImport(ctx *cli.Context) error {
return fatalError(err, "import failed")
}
logger.Info("Imported database from file")
slog.Info("Imported database from file")
return nil
}
@ -49,7 +49,7 @@ func cliRestore(ctx *cli.Context) error {
if err != nil {
return fatalError(err, "could not open import file for reading")
}
defer utils.Close(file, logger)
defer utils.Close(file)
inStream = file
}
@ -63,7 +63,7 @@ func cliRestore(ctx *cli.Context) error {
return fatalError(err, "restore failed")
}
logger.Info("Restored database from backup")
slog.Info("Restored database from backup")
return nil
}
@ -75,7 +75,7 @@ func cliExport(ctx *cli.Context) error {
if err != nil {
return fatalError(err, "could not open output file for writing")
}
defer utils.Close(file, logger)
defer utils.Close(file)
outStream = file
}
@ -89,6 +89,6 @@ func cliExport(ctx *cli.Context) error {
return fatalError(err, "export failed")
}
logger.Info("Exported database")
slog.Info("Exported database")
return nil
}

View File

@ -2,18 +2,15 @@ package database
import (
"context"
"encoding/json"
"errors"
"fmt"
jsoniter "github.com/json-iterator/go"
kv "github.com/strimertul/kilovolt/v11"
"go.uber.org/zap"
kv "git.sr.ht/~ashkeel/kilovolt/v12"
)
type CancelFunc func()
var json = jsoniter.ConfigFastest
var (
// ErrUnknown is returned when a response is received that doesn't match any expected outcome.
ErrUnknown = errors.New("unknown error")
@ -52,9 +49,9 @@ type KvPair struct {
Data string
}
func NewLocalClient(hub *kv.Hub, logger *zap.Logger) (*LocalDBClient, error) {
func NewLocalClient(hub *kv.Hub) (*LocalDBClient, error) {
// Create local client
localClient := kv.NewLocalClient(kv.ClientOptions{}, logger)
localClient := kv.NewLocalClient(kv.ClientOptions{})
// Run client and add it to the hub
go localClient.Run()

View File

@ -5,8 +5,9 @@ import (
"testing"
"time"
kv "git.sr.ht/~ashkeel/kilovolt/v12"
jsoniter "github.com/json-iterator/go"
kv "github.com/strimertul/kilovolt/v11"
)
func TestLocalDBClientPutKey(t *testing.T) {

View File

@ -6,11 +6,8 @@ import (
"os"
"path/filepath"
kv "github.com/strimertul/kilovolt/v11"
kv "git.sr.ht/~ashkeel/kilovolt/v12"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/utils"
)
// Driver is a driver wrapping a supported database
@ -49,13 +46,12 @@ func getDatabaseDriverName(ctx *cli.Context) string {
func GetDatabaseDriver(ctx *cli.Context) (Driver, error) {
name := getDatabaseDriverName(ctx)
dbDirectory := ctx.String("database-dir")
logger := ctx.Context.Value(utils.ContextLogger).(*zap.Logger)
switch name {
case "badger":
return nil, cli.Exit("Badger is not supported anymore as a database driver", 64)
case "pebble":
db, err := NewPebble(dbDirectory, logger)
db, err := NewPebble(dbDirectory)
if err != nil {
return nil, cli.Exit(err.Error(), 64)
}

View File

@ -1,27 +1,27 @@
package database
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
pebbledriver "git.sr.ht/~ashkeel/kilovolt-driver-pebble"
kv "git.sr.ht/~ashkeel/kilovolt/v12"
"git.sr.ht/~ashkeel/strimertul/utils"
pebble_driver "git.sr.ht/~ashkeel/kilovolt-driver-pebble"
"github.com/cockroachdb/pebble"
kv "github.com/strimertul/kilovolt/v11"
"go.uber.org/zap"
)
type PebbleDatabase struct {
db *pebble.DB
hub *kv.Hub
logger *zap.Logger
logger *slog.Logger
}
// NewPebble creates a new database driver instance with an underlying Pebble database
func NewPebble(directory string, logger *zap.Logger) (*PebbleDatabase, error) {
func NewPebble(directory string) (*PebbleDatabase, error) {
db, err := pebble.Open(directory, &pebble.Options{})
if err != nil {
return nil, fmt.Errorf("could not open DB: %w", err)
@ -34,9 +34,8 @@ func NewPebble(directory string, logger *zap.Logger) (*PebbleDatabase, error) {
}
p := &PebbleDatabase{
db: db,
hub: nil,
logger: logger,
db: db,
hub: nil,
}
return p, nil
@ -44,7 +43,9 @@ func NewPebble(directory string, logger *zap.Logger) (*PebbleDatabase, error) {
func (p *PebbleDatabase) Hub() *kv.Hub {
if p.hub == nil {
p.hub, _ = kv.NewHub(pebble_driver.NewPebbleBackend(p.db, true), kv.HubOptions{}, p.logger)
p.hub, _ = kv.NewHub(pebbledriver.NewPebbleBackend(p.db, true), kv.HubOptions{
Logger: p.logger,
})
}
return p.hub
}
@ -95,13 +96,13 @@ func (p *PebbleDatabase) Restore(file io.Reader) error {
func (p *PebbleDatabase) Backup(file io.Writer) error {
snapshot := p.db.NewSnapshot()
defer utils.Close(snapshot, p.logger)
defer utils.Close(snapshot)
iter, err := snapshot.NewIter(&pebble.IterOptions{})
if err != nil {
return err
}
defer utils.Close(iter, p.logger)
defer utils.Close(iter)
out := make(map[string]string)
for iter.First(); iter.Valid(); iter.Next() {

View File

@ -3,23 +3,20 @@ package database
import (
"testing"
kv "github.com/strimertul/kilovolt/v11"
"go.uber.org/zap/zaptest"
kv "git.sr.ht/~ashkeel/kilovolt/v12"
)
func CreateInMemoryLocalClient(t *testing.T) (*LocalDBClient, kv.Driver) {
logger := zaptest.NewLogger(t)
// Create in-memory store and hub
inMemoryStore := kv.MakeBackend()
hub, err := kv.NewHub(inMemoryStore, kv.HubOptions{}, logger)
hub, err := kv.NewHub(inMemoryStore, kv.HubOptions{})
if err != nil {
t.Fatal(err)
}
go hub.Run()
// Create local client
client, err := NewLocalClient(hub, logger)
client, err := NewLocalClient(hub)
if err != nil {
t.Fatal(err)
}

View File

@ -453,9 +453,9 @@
"dialog-title": "Application logs",
"levelFilter": "Filter per log severity",
"level": {
"info": "Info",
"warn": "Warning",
"error": "Error"
"INFO": "Info",
"WARN": "Warning",
"ERROR": "Error"
},
"copy-to-clipboard": "Copy to clipboard",
"copied": "Copied!",

View File

@ -36,9 +36,9 @@
"copy-to-clipboard": "Copia negli appunti",
"levelFilter": "Filtra per livello",
"level": {
"error": "Errore",
"info": "Info",
"warn": "Avvertimento"
"ERROR": "Errore",
"INFO": "Info",
"WARN": "Avvertimento"
},
"toggle-details": "Mostra dettagli"
},

View File

@ -4,7 +4,6 @@ import { main } from '@wailsapp/go/models';
export interface ProcessedLogEntry {
time: Date;
caller: string;
level: string;
message: string;
data: object;
@ -12,14 +11,12 @@ export interface ProcessedLogEntry {
export function processEntry({
time,
caller,
level,
message,
data,
}: main.LogEntry): ProcessedLogEntry {
return {
time: new Date(time),
caller,
level,
message,
data: JSON.parse(data) as object,
@ -34,12 +31,21 @@ const initialState: LoggingState = {
messages: [],
};
const keyfn = (ev: main.LogEntry) => ev.id;
const loggingReducer = createSlice({
name: 'logging',
initialState,
reducers: {
loadedLogData(state, { payload }: PayloadAction<main.LogEntry[]>) {
state.messages = payload
const logKeys = payload.map(keyfn);
// Clean up duplicates before setting to state
const uniqueLogs = payload.filter(
(ev, pos) => logKeys.indexOf(keyfn(ev)) === pos,
);
state.messages = uniqueLogs
.map(processEntry)
.sort((a, b) => a.time.getTime() - b.time.getTime());
},

View File

@ -46,12 +46,12 @@ const LogBubble = styled('div', {
},
variants: {
level: {
info: {},
warn: {
INFO: {},
WARN: {
backgroundColor: '$yellow6',
color: '$yellow11',
},
error: {
ERROR: {
backgroundColor: '$red6',
color: '$red11',
},
@ -60,12 +60,12 @@ const LogBubble = styled('div', {
});
const emptyFilter = {
info: false,
warn: false,
error: false,
INFO: false,
WARN: false,
ERROR: false,
};
type LogLevel = keyof typeof emptyFilter;
const levels: LogLevel[] = ['info', 'warn', 'error'];
const levels: LogLevel[] = ['INFO', 'WARN', 'ERROR'];
function isSupportedLevel(level: string): level is LogLevel {
return (levels as string[]).includes(level);
@ -89,7 +89,7 @@ const LevelToggle = styled(MultiToggleItem, {
},
variants: {
level: {
info: {
INFO: {
backgroundColor: '$gray4',
[`.${lightMode} &`]: {
backgroundColor: '$gray2',
@ -112,7 +112,7 @@ const LevelToggle = styled(MultiToggleItem, {
},
},
},
warn: {
WARN: {
backgroundColor: '$yellow4',
[`.${lightMode} &`]: {
backgroundColor: '$yellow2',
@ -135,7 +135,7 @@ const LevelToggle = styled(MultiToggleItem, {
},
},
},
error: {
ERROR: {
backgroundColor: '$red4',
[`.${lightMode} &`]: {
backgroundColor: '$red2',
@ -175,11 +175,11 @@ const LogEntryContainer = styled('div', {
fontSize: '0.9em',
variants: {
level: {
info: {},
warn: {
INFO: {},
WARN: {
backgroundColor: '$yellow4',
},
error: {
ERROR: {
backgroundColor: '$red6',
},
},
@ -199,12 +199,12 @@ const LogTime = styled('div', {
borderBottomLeftRadius: theme.borderRadius.form,
variants: {
level: {
info: {},
warn: {
INFO: {},
WARN: {
color: '$yellow11',
backgroundColor: '$yellow6',
},
error: {
ERROR: {
color: '$red11',
backgroundColor: '$red7',
},
@ -230,13 +230,13 @@ const LogActions = styled('div', {
},
variants: {
level: {
info: {},
warn: {
INFO: {},
WARN: {
'& a:hover': {
color: '$yellow11',
},
},
error: {
ERROR: {
'& a:hover': {
color: '$red11',
},
@ -258,11 +258,11 @@ const LogDetails = styled('div', {
borderBottomLeftRadius: theme.borderRadius.form,
variants: {
level: {
info: {},
warn: {
INFO: {},
WARN: {
backgroundColor: '$yellow3',
},
error: {
ERROR: {
backgroundColor: '$red4',
},
},
@ -276,11 +276,11 @@ const LogDetailKey = styled('div', {
color: '$teal10',
variants: {
level: {
info: {},
warn: {
INFO: {},
WARN: {
color: '$yellow11',
},
error: {
ERROR: {
color: '$red11',
},
},
@ -430,10 +430,7 @@ function LogDialog({ initialFilter }: LogDialogProps) {
>
<LogEntriesContainer>
{filtered.reverse().map((entry) => (
<LogItem
key={entry.caller + entry.time.getTime().toString()}
data={entry}
/>
<LogItem key={entry.time.getTime().toString()} data={entry} />
))}
</LogEntriesContainer>
</Scrollbar>

View File

@ -469,7 +469,7 @@ function TwitchSection() {
const onKeyChange = (value: string) => {
const event = JSON.parse(value) as EventSubNotification;
void setCleanTwitchEvents((prev) => [event, ...prev]);
void setCleanTwitchEvents([event, ...twitchEvents]);
};
void kv.subscribePrefix('twitch/ev/eventsub-event/', onKeyChange);

View File

@ -73,7 +73,7 @@ export namespace main {
}
}
export class LogEntry {
caller: string;
id: string;
time: string;
level: string;
message: string;
@ -85,7 +85,7 @@ export namespace main {
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.caller = source["caller"];
this.id = source["id"];
this.time = source["time"];
this.level = source["level"];
this.message = source["message"];

7
go.mod
View File

@ -6,7 +6,8 @@ toolchain go1.22.0
require (
git.sr.ht/~ashkeel/containers v0.3.6
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.2.5
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.3.1
git.sr.ht/~ashkeel/kilovolt/v12 v12.0.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
github.com/cockroachdb/pebble v1.1.0
@ -14,10 +15,9 @@ require (
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/json-iterator/go v1.1.12
github.com/nicklaw5/helix/v2 v2.28.1
github.com/strimertul/kilovolt/v11 v11.0.1
github.com/samber/slog-multi v1.0.2
github.com/urfave/cli/v2 v2.27.1
github.com/wailsapp/wails/v2 v2.8.0
go.uber.org/zap v1.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
@ -79,7 +79,6 @@ require (
github.com/wailsapp/go-webview2 v1.0.10 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/net v0.20.0 // indirect

16
go.sum
View File

@ -33,8 +33,10 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.sr.ht/~ashkeel/containers v0.3.6 h1:+umWlQGKhLxGQlaEUt/F6rBZGpeBd1T01fM3wro+qTY=
git.sr.ht/~ashkeel/containers v0.3.6/go.mod h1:i2KocnJfRH0FwfgPi4nw7/ehYLEoLlP3iwdDoBeVdME=
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.2.5 h1:btFQjGLisyJwwLgzQ80l39kGlKyCHkVTnbGMnlRwxIk=
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.2.5/go.mod h1:MPfBoPjCurdVQzwvSg49VDkeS/B+Ja96JnZ5cykksL8=
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.3.1 h1:AQORTHRA6Ce1TW24FgHN1K5hWT8+/5nC7sJ5L2W+tRM=
git.sr.ht/~ashkeel/kilovolt-driver-pebble v1.3.1/go.mod h1:lPYwJ9bumCDzeklkhXh0OCasiB7omzW+ykaLUZ30waE=
git.sr.ht/~ashkeel/kilovolt/v12 v12.0.0 h1:whWstBXDkj3WEEDpQu3+zTWYZNlJtmlDcUkAnkleY4g=
git.sr.ht/~ashkeel/kilovolt/v12 v12.0.0/go.mod h1:FGg4hwyY/e0G/mpLgUA/nYVIpHUTc4uFIwesM/MJVtA=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
@ -297,6 +299,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/slog-multi v1.0.2 h1:6BVH9uHGAsiGkbbtQgAOQJMpKgV8unMrHhhJaw+X1EQ=
github.com/samber/slog-multi v1.0.2/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@ -313,8 +317,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/strimertul/kilovolt/v11 v11.0.1 h1:fV33Z3b168LeSROzzV0dZcI2Jptg3TvF2FbEGxfEVGI=
github.com/strimertul/kilovolt/v11 v11.0.1/go.mod h1:PjhGVWb74lB8dXSGWA7GmVSbZAoGV/WGGmjS2Zz/UBg=
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
@ -342,12 +344,6 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -1,53 +1,49 @@
package main
import (
"context"
"fmt"
"log/slog"
"math/rand"
"os"
"time"
"git.sr.ht/~ashkeel/containers/sync"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
slogmulti "github.com/samber/slog-multi"
"gopkg.in/natefinch/lumberjack.v2"
)
const LogHistory = 50
var (
logger *zap.Logger
lastLogs *sync.Slice[LogEntry]
incomingLogs chan LogEntry
)
func initLogger(level zapcore.Level) {
func initLogger(level slog.Level) {
lastLogs = sync.NewSlice[LogEntry]()
incomingLogs = make(chan LogEntry, 100)
logStorage := NewLogStorage(level)
consoleLogger := zapcore.NewCore(
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
zapcore.Lock(os.Stderr),
level,
)
fileLogger := zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
zapcore.AddSync(&lumberjack.Logger{
Filename: logFilename,
MaxSize: 20,
MaxBackups: 3,
MaxAge: 28,
}),
level,
)
core := zapcore.NewTee(
consoleLogger,
fileLogger,
logStorage,
)
logger = zap.New(core, zap.AddCaller())
fileLogger := &lumberjack.Logger{
Filename: logFilename,
MaxSize: 20,
MaxBackups: 3,
MaxAge: 28,
}
consoleHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: level,
})
fileHandler := slog.NewJSONHandler(fileLogger, &slog.HandlerOptions{
AddSource: true,
Level: level,
})
logger := slog.New(slogmulti.Fanout(consoleHandler, fileHandler, logStorage))
slog.SetDefault(logger)
}
type LogEntry struct {
Caller string `json:"caller"`
ID string `json:"id"`
Time string `json:"time"`
Level string `json:"level"`
Message string `json:"message"`
@ -55,42 +51,32 @@ type LogEntry struct {
}
type LogStorage struct {
zapcore.LevelEnabler
fields []zapcore.Field
encoder zapcore.Encoder
minLevel slog.Level
attrs []slog.Attr
group string
}
func NewLogStorage(enabler zapcore.LevelEnabler) *LogStorage {
return &LogStorage{
LevelEnabler: enabler,
encoder: zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()),
}
func (core *LogStorage) Enabled(_ context.Context, level slog.Level) bool {
return level >= core.minLevel
}
func (core *LogStorage) With(fields []zapcore.Field) zapcore.Core {
clone := *core
clone.fields = append(clone.fields, fields...)
return &clone
}
func (core *LogStorage) Handle(_ context.Context, record slog.Record) error {
attributes := map[string]any{}
record.Attrs(func(attrs slog.Attr) bool {
attributes[attrs.Key] = attrs.Value.Any()
return true
})
attrJSON, _ := json.MarshalToString(attributes)
func (core *LogStorage) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if core.Enabled(entry.Level) {
return checked.AddCore(entry, core)
}
return checked
}
// Generate unique log ID
id := fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Int31())
func (core *LogStorage) Write(entry zapcore.Entry, fields []zapcore.Field) error {
buf, err := core.encoder.EncodeEntry(entry, append(core.fields, fields...))
if err != nil {
return err
}
logEntry := LogEntry{
Caller: entry.Caller.String(),
Time: entry.Time.Format(time.RFC3339),
Level: entry.Level.String(),
Message: entry.Message,
Data: buf.String(),
ID: id,
Time: record.Time.Format(time.RFC3339),
Level: record.Level.String(),
Message: record.Message,
Data: attrJSON,
}
lastLogs.Push(logEntry)
if lastLogs.Size() > LogHistory {
@ -100,13 +86,32 @@ func (core *LogStorage) Write(entry zapcore.Entry, fields []zapcore.Field) error
return nil
}
func (core *LogStorage) Sync() error {
return nil
func (core *LogStorage) WithAttrs(attrs []slog.Attr) slog.Handler {
return &LogStorage{
minLevel: core.minLevel,
attrs: append(core.attrs, attrs...),
group: core.group,
}
}
func parseAsFields(fields map[string]any) (result []zapcore.Field) {
for k, v := range fields {
result = append(result, zap.Any(k, v))
func (core *LogStorage) WithGroup(name string) slog.Handler {
return &LogStorage{
minLevel: core.minLevel,
attrs: core.attrs,
group: name,
}
return
}
func NewLogStorage(level slog.Level) *LogStorage {
return &LogStorage{
minLevel: level,
}
}
func parseLogFields(data map[string]any) []any {
fields := []any{}
for k, v := range data {
fields = append(fields, k, v)
}
return fields
}

View File

@ -4,15 +4,14 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"time"
"git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/utils"
jsoniter "github.com/json-iterator/go"
)
var json = jsoniter.ConfigFastest
@ -31,7 +30,6 @@ type Manager struct {
Goals *sync.Slice[Goal]
Queue *sync.Slice[Redeem]
db database.Database
logger *zap.Logger
cooldowns map[string]time.Time
banlist map[string]bool
ctx context.Context
@ -40,7 +38,7 @@ type Manager struct {
restartTwitchHandler chan struct{}
}
func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) {
func NewManager(db database.Database) (*Manager, error) {
ctx, cancelFn := context.WithCancel(context.Background())
loyalty := &Manager{
Config: sync.NewRWSync(Config{Enabled: false}),
@ -48,7 +46,6 @@ func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) {
Goals: sync.NewSlice[Goal](),
Queue: sync.NewSlice[Redeem](),
logger: logger,
db: db,
points: sync.NewMap[string, PointsEntry](),
cooldowns: make(map[string]time.Time),
@ -117,7 +114,7 @@ func NewManager(db database.Database, logger *zap.Logger) (*Manager, error) {
// SubscribePrefix for changes
loyalty.cancelSub, err = db.SubscribePrefix(loyalty.update, "loyalty/")
if err != nil {
logger.Error("Could not setup loyalty reload subscription", zap.Error(err))
slog.Error("Could not setup loyalty reload subscription", "error", err)
}
loyalty.SetBanList(config.BanList)
@ -177,9 +174,9 @@ func (m *Manager) update(key, value string) {
}
}
if err != nil {
m.logger.Error("Subscribe error: invalid JSON received on key", zap.Error(err), zap.String("key", key))
slog.Error("Subscribe error: invalid JSON received on key", slog.String("key", key), "error", err)
} else {
m.logger.Debug("Updated key", zap.String("key", key))
slog.Debug("Updated key", slog.String("key", key))
}
}

45
main.go
View File

@ -5,9 +5,12 @@ import (
"embed"
"fmt"
"log"
"log/slog"
_ "net/http/pprof"
"os"
"runtime/debug"
"git.sr.ht/~ashkeel/strimertul/utils"
"github.com/apenwarr/fixconsole"
jsoniter "github.com/json-iterator/go"
"github.com/urfave/cli/v2"
@ -15,15 +18,13 @@ import (
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"git.sr.ht/~ashkeel/strimertul/utils"
)
var json = jsoniter.ConfigFastest
var appVersion = "v0.0.0-UNKNOWN"
const devVersionMarker = "v0.0.0-UNKNOWN"
var appVersion = devVersionMarker
const (
crashReportURL = "https://crash.strimertul.stream/upload"
@ -35,8 +36,7 @@ const (
var frontend embed.FS
func main() {
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
if err := fixconsole.FixConsoleIfNeeded(); err != nil {
log.Fatal(err)
}
@ -86,32 +86,34 @@ func main() {
},
Before: func(ctx *cli.Context) error {
// Initialize logger with global flags
level, err := zapcore.ParseLevel(ctx.String("log-level"))
if err != nil {
level = zapcore.InfoLevel
level := slog.LevelInfo
if err := level.UnmarshalText([]byte(ctx.String("log-level"))); err != nil {
return cli.Exit(fmt.Sprintf("Invalid log level: %s", err), 1)
}
initLogger(level)
// Create file for panics
var err error
panicLog, err = os.OpenFile(panicFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
logger.Warn("Could not create panic log", zap.Error(err))
slog.Warn("Could not create panic log", "error", err)
} else {
utils.RedirectStderr(panicLog)
}
zap.RedirectStdLog(logger)()
// For development builds, force crash dumps
if isDev() {
debug.SetTraceback("crash")
}
ctx.Context = context.WithValue(ctx.Context, utils.ContextLogger, logger)
return nil
},
After: func(_ *cli.Context) error {
if panicLog != nil {
utils.Close(panicLog, logger)
utils.Close(panicLog)
}
_ = logger.Sync()
return nil
},
}
@ -164,13 +166,18 @@ func cliMain(ctx *cli.Context) error {
return nil
}
func warnOnError(err error, text string, fields ...zap.Field) {
func warnOnError(err error, text string, fields ...any) {
if err != nil {
fields = append(fields, zap.Error(err))
logger.Warn(text, fields...)
fields = append(fields, "error", err)
slog.Warn(text, fields...)
}
}
func fatalError(err error, text string) error {
return cli.Exit(fmt.Errorf("%s: %w", text, err), 1)
}
// isDev checks if the running code is a development version
func isDev() bool {
return appVersion == devVersionMarker
}

View File

@ -4,10 +4,9 @@ import (
"bytes"
"errors"
"fmt"
"log/slog"
"strconv"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database"
)
@ -33,18 +32,18 @@ func GetCurrentSchemaVersion(db database.Database) (int, error) {
return strconv.Atoi(versionStr)
}
func Run(driver database.Driver, db database.Database, logger *zap.Logger) (err error) {
func Run(driver database.Driver, db database.Database, logger *slog.Logger) (err error) {
// Make a backup of the database
var buffer bytes.Buffer
if err = driver.Backup(&buffer); err != nil {
return fmt.Errorf("failed to backup database: %s", err)
return fmt.Errorf("failed to backup database: %w", err)
}
// Restore the backup if an error occurs
defer func() {
if err != nil {
restoreErr := driver.Restore(bytes.NewReader(buffer.Bytes()))
if restoreErr != nil {
logger.Error("Failed to restore database from backup", zap.Error(restoreErr))
logger.Error("Failed to restore database from backup", "error", restoreErr)
}
}
}()

View File

@ -2,6 +2,7 @@ package migrations
import (
"encoding/json"
"log/slog"
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
@ -10,10 +11,9 @@ import (
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
"git.sr.ht/~ashkeel/strimertul/twitch/timers"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
)
func migrateToV4(db database.Database, logger *zap.Logger) error {
func migrateToV4(db database.Database, logger *slog.Logger) error {
logger.Info("Migrating database from v3 to v4")
// Rename keys that have no schema changes

View File

@ -1,8 +1,9 @@
package main
import (
"log/slog"
"git.sr.ht/~ashkeel/strimertul/twitch"
"go.uber.org/zap"
)
type ProblemID string
@ -26,7 +27,7 @@ func (a *App) GetProblems() (problems []Problem) {
// Check if the app needs to be authorized again
scopesMatch, err := twitch.CheckScopes(client.DB)
if err != nil {
logger.Warn("Could not check scopes for problems", zap.Error(err))
slog.Warn("Could not check scopes for problems", "error", err)
} else {
if !scopesMatch {
problems = append(problems, Problem{

View File

@ -1,19 +1,18 @@
package alerts
import (
"encoding/json"
"math/rand"
"text/template"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
"github.com/nicklaw5/helix/v2"
)
func (m *Module) onEventSubEvent(_ string, value string) {
var ev eventSubNotification
if err := json.UnmarshalFromString(value, &ev); err != nil {
m.logger.Warn("Error parsing webhook payload", zap.Error(err))
if err := json.Unmarshal([]byte(value), &ev); err != nil {
m.logger.Warn("Error parsing webhook payload", "error", err)
return
}
switch ev.Subscription.Type {
@ -25,7 +24,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
// Parse as a follow event
var followEv helix.EventSubChannelFollowEvent
if err := json.Unmarshal(ev.Event, &followEv); err != nil {
m.logger.Warn("Error parsing follow event", zap.Error(err))
m.logger.Warn("Error parsing follow event", "error", err)
return
}
// Pick a random message
@ -50,7 +49,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
var raidEv helix.EventSubChannelRaidEvent
if err := json.Unmarshal(ev.Event, &raidEv); err != nil {
m.logger.Warn("Error parsing raid event", zap.Error(err))
m.logger.Warn("Error parsing raid event", "error", err)
return
}
// Pick a random message from base set
@ -84,7 +83,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
// Parse as cheer event
var cheerEv helix.EventSubChannelCheerEvent
if err := json.Unmarshal(ev.Event, &cheerEv); err != nil {
m.logger.Warn("Error parsing cheer event", zap.Error(err))
m.logger.Warn("Error parsing cheer event", "error", err)
return
}
// Pick a random message from base set
@ -118,7 +117,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
// Parse as subscription event
var subEv helix.EventSubChannelSubscribeEvent
if err := json.Unmarshal(ev.Event, &subEv); err != nil {
m.logger.Warn("Error parsing new subscription event", zap.Error(err))
m.logger.Warn("Error parsing new subscription event", "error", err)
return
}
m.addMixedEvent(subEv)
@ -131,7 +130,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
var subEv helix.EventSubChannelSubscriptionMessageEvent
err := json.Unmarshal(ev.Event, &subEv)
if err != nil {
m.logger.Warn("Error parsing returning subscription event", zap.Error(err))
m.logger.Warn("Error parsing returning subscription event", "error", err)
return
}
m.addMixedEvent(subEv)
@ -143,7 +142,7 @@ func (m *Module) onEventSubEvent(_ string, value string) {
// Parse as gift event
var giftEv helix.EventSubChannelSubscriptionGiftEvent
if err := json.Unmarshal(ev.Event, &giftEv); err != nil {
m.logger.Warn("Error parsing subscription gifted event", zap.Error(err))
m.logger.Warn("Error parsing subscription gifted event", "error", err)
return
}
// Pick a random message from base set

View File

@ -2,19 +2,16 @@ package alerts
import (
"context"
"encoding/json"
"log/slog"
"sync"
"text/template"
jsoniter "github.com/json-iterator/go"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
template2 "git.sr.ht/~ashkeel/strimertul/twitch/template"
)
var json = jsoniter.ConfigFastest
type (
templateCache map[string]*template.Template
templateCacheMap map[templateType]templateCache
@ -35,7 +32,7 @@ type Module struct {
ctx context.Context
db database.Database
logger *zap.Logger
logger *slog.Logger
templater template2.Engine
templates templateCacheMap
@ -43,7 +40,7 @@ type Module struct {
pendingSubs map[string]subMixedEvent
}
func Setup(ctx context.Context, db database.Database, logger *zap.Logger, templater template2.Engine) *Module {
func Setup(ctx context.Context, db database.Database, logger *slog.Logger, templater template2.Engine) *Module {
mod := &Module{
ctx: ctx,
db: db,
@ -55,31 +52,31 @@ func Setup(ctx context.Context, db database.Database, logger *zap.Logger, templa
// Load config from database
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
logger.Debug("Config load error", zap.Error(err))
logger.Debug("Config load error", "error", err)
mod.Config = Config{}
// Save empty config
err = db.PutJSON(ConfigKey, mod.Config)
if err != nil {
logger.Warn("Could not save default config for bot alerts", zap.Error(err))
logger.Warn("Could not save default config for bot alerts", "error", err)
}
}
mod.compileTemplates()
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
err := json.UnmarshalFromString(value, &mod.Config)
err := json.Unmarshal([]byte(value), &mod.Config)
if err != nil {
logger.Warn("Error loading alert config", zap.Error(err))
logger.Warn("Error loading alert config", "error", err)
} else {
logger.Info("Reloaded alert config")
}
mod.compileTemplates()
}); err != nil {
logger.Error("Could not set-up bot alert reload subscription", zap.Error(err))
logger.Error("Could not set-up bot alert reload subscription", "error", err)
}
if err := db.SubscribePrefixContext(ctx, mod.onEventSubEvent, eventsub.EventKeyPrefix); err != nil {
logger.Error("Could not setup twitch alert subscription", zap.Error(err))
logger.Error("Could not setup twitch alert subscription", "error", err)
}
logger.Debug("Loaded bot alerts")

View File

@ -4,8 +4,6 @@ import (
"bytes"
"text/template"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
)
@ -44,7 +42,7 @@ func (m *Module) compileTemplates() {
func (m *Module) addTemplate(templateList templateCache, message string) {
tpl, err := m.templater.MakeTemplate(message)
if err != nil {
m.logger.Error("Error compiling alert template", zap.Error(err))
m.logger.Error("Error compiling alert template", "error", err)
return
}
templateList[message] = tpl
@ -60,7 +58,7 @@ func (m *Module) addTemplatesForType(templateList templateType, messages []strin
func (m *Module) writeTemplate(tpl *template.Template, data interface{}, announce bool) {
var buf bytes.Buffer
if err := tpl.Execute(&buf, data); err != nil {
m.logger.Error("Error executing template for bot alert", zap.Error(err))
m.logger.Error("Error executing template for bot alert", "error", err)
return
}
chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{

View File

@ -2,9 +2,9 @@ package chat
import (
"bytes"
"log/slog"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
)
var accessLevels = map[AccessLevelType]int{
@ -70,7 +70,7 @@ func cmdCustom(mod *Module, cmd string, data CustomCommand, message helix.EventS
return
}
if err := tpl.Execute(&buf, message); err != nil {
mod.logger.Error("Failed to execute custom command template", zap.Error(err))
mod.logger.Error("Failed to execute custom command template", "error", err)
return
}
@ -97,7 +97,7 @@ func cmdCustom(mod *Module, cmd string, data CustomCommand, message helix.EventS
Announce: true,
}
default:
mod.logger.Error("Unknown response type", zap.String("type", string(data.ResponseType)))
mod.logger.Error("Unknown response type", slog.String("type", string(data.ResponseType)))
}
WriteMessage(mod.db, mod.logger, request)

View File

@ -12,10 +12,8 @@ import (
"git.sr.ht/~ashkeel/containers/sync"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/loyalty"
"github.com/nicklaw5/helix/v2"
)
const (
@ -91,7 +89,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
var streamInfos []helix.Stream
err := mod.db.GetJSON(twitch.StreamInfoKey, &streamInfos)
if err != nil {
mod.logger.Error("Error retrieving stream info", zap.Error(err))
mod.logger.Error("Error retrieving stream info", "error", err)
continue
}
if len(streamInfos) < 1 {
@ -104,7 +102,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
for {
userClient, err := twitch.GetUserClient(mod.db, false)
if err != nil {
mod.logger.Error("Could not get user api client for list of chatters", zap.Error(err))
mod.logger.Error("Could not get user api client for list of chatters", "error", err)
return
}
res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{
@ -114,7 +112,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
After: cursor,
})
if err != nil {
mod.logger.Error("Could not retrieve list of chatters", zap.Error(err))
mod.logger.Error("Could not retrieve list of chatters", "error", err)
return
}
for _, user := range res.Data.Chatters {
@ -150,7 +148,7 @@ func setupLoyaltyIntegration(ctx context.Context, mod *Module, manager *loyalty.
if len(users) > 0 {
err := li.manager.GivePoints(pointsToGive)
if err != nil {
mod.logger.Error("Error awarding loyalty points to user", zap.Error(err))
mod.logger.Error("Error awarding loyalty points to user", "error", err)
}
}
}
@ -244,7 +242,7 @@ func (li *loyaltyIntegration) cmdRedeemReward(message helix.EventSubChannelChatM
})
return
}
li.module.logger.Error("Error while performing redeem", zap.Error(err))
li.module.logger.Error("Error while performing redeem", "error", err)
return
}
@ -258,7 +256,7 @@ func (li *loyaltyIntegration) cmdGoalList(message helix.EventSubChannelChatMessa
goals := li.manager.Goals.Get()
if len(goals) < 1 {
li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("There are no active community goals right now :(!"),
Message: "There are no active community goals right now :(!",
ReplyTo: message.MessageID,
})
return
@ -300,12 +298,12 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
if goalIndex < 0 {
if hasGoals {
li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("All active community goals have been reached already! NewRecord"),
Message: "All active community goals have been reached already! NewRecord",
ReplyTo: message.MessageID,
})
} else {
li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("There are no active community goals right now :(!"),
Message: "There are no active community goals right now :(!",
ReplyTo: message.MessageID,
})
}
@ -319,7 +317,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
if err == nil {
if newPoints <= 0 {
li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("Nice try SoBayed"),
Message: "Nice try SoBayed",
ReplyTo: message.MessageID,
})
return
@ -343,7 +341,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
// Invalid goal ID provided
if !found {
li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("I couldn't find that goal ID :("),
Message: "I couldn't find that goal ID :(",
ReplyTo: message.MessageID,
})
return
@ -357,7 +355,7 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
// Check if goal was reached already
if selectedGoal.Contributed >= selectedGoal.TotalGoal {
li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("This goal was already reached! ヾ(•ω•`)o"),
Message: "This goal was already reached! ヾ(•ω•`)o",
ReplyTo: message.MessageID,
})
return
@ -366,12 +364,12 @@ func (li *loyaltyIntegration) cmdContributeGoal(message helix.EventSubChannelCha
// Add points to goal
points, err := li.manager.PerformContribution(selectedGoal, message.ChatterUserLogin, points)
if err != nil {
li.module.logger.Error("Error while contributing to goal", zap.Error(err))
li.module.logger.Error("Error while contributing to goal", "error", err)
return
}
if points == 0 {
li.module.WriteMessage(WriteMessageRequest{
Message: fmt.Sprintf("Sorry but you're broke"),
Message: "Sorry but you're broke",
ReplyTo: message.MessageID,
})
return

View File

@ -3,19 +3,18 @@ package chat
import (
"context"
"errors"
"log/slog"
"strings"
textTemplate "text/template"
"time"
"git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
"git.sr.ht/~ashkeel/strimertul/twitch/template"
"git.sr.ht/~ashkeel/strimertul/utils"
jsoniter "github.com/json-iterator/go"
"github.com/nicklaw5/helix/v2"
)
var json = jsoniter.ConfigFastest
@ -27,7 +26,7 @@ type Module struct {
db database.Database
api *helix.Client
user helix.User
logger *zap.Logger
logger *slog.Logger
templater template.Engine
lastMessage *sync.RWSync[time.Time]
@ -35,11 +34,9 @@ type Module struct {
customCommands *sync.Map[string, CustomCommand]
customTemplates *sync.Map[string, *textTemplate.Template]
customFunctions textTemplate.FuncMap
cancelContext context.CancelFunc
}
func Setup(ctx context.Context, db database.Database, api *helix.Client, user helix.User, logger *zap.Logger, templater template.Engine) *Module {
func Setup(ctx context.Context, db database.Database, api *helix.Client, user helix.User, logger *slog.Logger, templater template.Engine) *Module {
mod := &Module{
ctx: ctx,
db: db,
@ -62,12 +59,12 @@ func Setup(ctx context.Context, db database.Database, api *helix.Client, user he
CommandCooldown: 2,
}
} else {
logger.Error("Failed to load chat module config", zap.Error(err))
logger.Error("Failed to load chat module config", "error", err)
}
}
if err := db.SubscribeKeyContext(ctx, eventsub.EventKeyPrefix+helix.EventSubTypeChannelChatMessage, mod.onChatMessage); err != nil {
logger.Error("Could not subscribe to chat messages", zap.Error(err))
logger.Error("Could not subscribe to chat messages", "error", err)
}
// Load custom commands
@ -76,21 +73,21 @@ func Setup(ctx context.Context, db database.Database, api *helix.Client, user he
if errors.Is(err, database.ErrEmptyKey) {
customCommands = make(map[string]CustomCommand)
} else {
logger.Error("Failed to load custom commands", zap.Error(err))
logger.Error("Failed to load custom commands", "error", err)
}
}
mod.customCommands.Set(customCommands)
if err := mod.updateTemplates(); err != nil {
logger.Error("Failed to parse custom commands", zap.Error(err))
logger.Error("Failed to parse custom commands", "error", err)
}
if err := db.SubscribeKeyContext(ctx, CustomCommandsKey, mod.updateCommands); err != nil {
logger.Error("Could not set-up chat command reload subscription", zap.Error(err))
logger.Error("Could not set-up chat command reload subscription", "error", err)
}
if err := db.SubscribeKeyContext(ctx, WriteMessageRPC, mod.handleWriteMessageRPC); err != nil {
logger.Error("Could not set-up chat command reload subscription", zap.Error(err))
logger.Error("Could not set-up chat command reload subscription", "error", err)
}
return mod
@ -101,7 +98,7 @@ func (mod *Module) onChatMessage(newValue string) {
Event helix.EventSubChannelChatMessageEvent `json:"event"`
}
if err := json.UnmarshalFromString(newValue, &chatMessage); err != nil {
mod.logger.Error("Failed to decode incoming chat message", zap.Error(err))
mod.logger.Error("Failed to decode incoming chat message", "error", err)
return
}
@ -149,7 +146,7 @@ func (mod *Module) onChatMessage(newValue string) {
func (mod *Module) handleWriteMessageRPC(value string) {
var request WriteMessageRequest
if err := json.Unmarshal([]byte(value), &request); err != nil {
mod.logger.Warn("Failed to decode write message request", zap.Error(err))
mod.logger.Warn("Failed to decode write message request", "error", err)
return
}
@ -160,10 +157,10 @@ func (mod *Module) handleWriteMessageRPC(value string) {
Message: request.Message,
})
if err != nil {
mod.logger.Error("Failed to send announcement", zap.Error(err))
mod.logger.Error("Failed to send announcement", "error", err)
}
if resp.Error != "" {
mod.logger.Error("Failed to send announcement", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage))
mod.logger.Error("Failed to send announcement", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
}
return
}
@ -175,10 +172,10 @@ func (mod *Module) handleWriteMessageRPC(value string) {
Message: request.Message,
})
if err != nil {
mod.logger.Error("Failed to send whisper", zap.Error(err))
mod.logger.Error("Failed to send whisper", "error", err)
}
if resp.Error != "" {
mod.logger.Error("Failed to send whisper", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage))
mod.logger.Error("Failed to send whisper", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
}
return
}
@ -190,22 +187,22 @@ func (mod *Module) handleWriteMessageRPC(value string) {
ReplyParentMessageID: request.ReplyTo,
})
if err != nil {
mod.logger.Error("Failed to send chat message", zap.Error(err))
mod.logger.Error("Failed to send chat message", "error", err)
}
if resp.Error != "" {
mod.logger.Error("Failed to send chat message", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage))
mod.logger.Error("Failed to send chat message", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
}
}
func (mod *Module) updateCommands(value string) {
err := utils.LoadJSONToWrapped[map[string]CustomCommand](value, mod.customCommands)
if err != nil {
mod.logger.Error("Failed to decode new custom commands", zap.Error(err))
mod.logger.Error("Failed to decode new custom commands", "error", err)
return
}
// Recreate templates
if err := mod.updateTemplates(); err != nil {
mod.logger.Error("Failed to update custom commands templates", zap.Error(err))
mod.logger.Error("Failed to update custom commands templates", "error", err)
return
}
}

View File

@ -1,8 +1,9 @@
package chat
import (
"log/slog"
"git.sr.ht/~ashkeel/strimertul/database"
"go.uber.org/zap"
)
// WriteMessageRequest is an RPC to send a chat message with extra options
@ -13,9 +14,9 @@ type WriteMessageRequest struct {
Announce bool `json:"announce" desc:"If true, send as announcement"`
}
func WriteMessage(db database.Database, logger *zap.Logger, m WriteMessageRequest) {
func WriteMessage(db database.Database, logger *slog.Logger, m WriteMessageRequest) {
err := db.PutJSON(WriteMessageRPC, m)
if err != nil {
logger.Error("Failed to write chat message", zap.Error(err))
logger.Error("Failed to write chat message", "error", err)
}
}

View File

@ -2,14 +2,14 @@ package client
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"time"
"git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/twitch"
@ -20,13 +20,11 @@ import (
"git.sr.ht/~ashkeel/strimertul/webserver"
)
var json = jsoniter.ConfigFastest
type Manager struct {
client *Client
}
func NewManager(ctx context.Context, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Manager, error) {
func NewManager(ctx context.Context, db database.Database, server *webserver.WebServer) (*Manager, error) {
// Get Twitch config
var config twitch.Config
if err := db.GetJSON(twitch.ConfigKey, &config); err != nil {
@ -38,7 +36,7 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
// Create new client
clientContext, cancel := context.WithCancel(ctx)
client, err := newClient(clientContext, config, db, server, logger)
client, err := newClient(clientContext, config, db, server)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create twitch client: %w", err)
@ -51,8 +49,8 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
// Listen for client config changes
if err = db.SubscribeKeyContext(ctx, twitch.ConfigKey, func(value string) {
var newConfig twitch.Config
if err := json.UnmarshalFromString(value, &newConfig); err != nil {
logger.Error("Failed to decode Twitch integration config", zap.Error(err))
if err := json.Unmarshal([]byte(value), &newConfig); err != nil {
slog.Error("Failed to decode Twitch integration config", "error", err)
return
}
@ -60,9 +58,9 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
var updatedClient *Client
clientContext, cancel = context.WithCancel(ctx)
updatedClient, err = newClient(clientContext, newConfig, db, server, logger)
updatedClient, err = newClient(clientContext, newConfig, db, server)
if err != nil {
logger.Error("Could not create twitch client with new config, keeping old", zap.Error(err))
slog.Error("Could not create twitch client with new config, keeping old", "error", err)
return
}
@ -70,9 +68,9 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
updatedClient.Merge(manager.client)
manager.client = updatedClient
logger.Info("Reloaded/updated Twitch integration")
slog.Info("Reloaded/updated Twitch integration")
}); err != nil {
logger.Error("Could not setup twitch config reload subscription", zap.Error(err))
slog.Error("Could not setup twitch config reload subscription", "error", err)
}
return manager, nil
@ -87,7 +85,7 @@ type Client struct {
DB database.Database
API *helix.Client
User helix.User
Logger *zap.Logger
Logger *slog.Logger
Chat *chat.Module
Alerts *alerts.Module
@ -115,12 +113,12 @@ func (c *Client) ensureRoute() {
}
}
func newClient(ctx context.Context, config twitch.Config, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Client, error) {
func newClient(ctx context.Context, config twitch.Config, db database.Database, server *webserver.WebServer) (*Client, error) {
// Create Twitch client
client := &Client{
Config: sync.NewRWSync(config),
DB: db,
Logger: logger.With(zap.String("service", "twitch")),
Logger: slog.With(slog.String("service", "twitch")),
restart: make(chan bool, 128),
streamOnline: sync.NewRWSync(false),
ctx: ctx,
@ -142,21 +140,21 @@ func newClient(ctx context.Context, config twitch.Config, db database.Database,
if userClient, err := twitch.GetUserClient(db, true); err == nil {
users, err := userClient.GetUsers(&helix.UsersParams{})
if err != nil {
client.Logger.Error("Failed looking up user", zap.Error(err))
client.Logger.Error("Failed looking up user", "error", err)
} else if len(users.Data.Users) < 1 {
client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
} else {
client.Logger.Info("Twitch user identified", zap.String("user", users.Data.Users[0].ID))
client.Logger.Info("Twitch user identified", slog.String("user", users.Data.Users[0].ID))
client.User = users.Data.Users[0]
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, logger)
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, client.Logger)
if err != nil {
client.Logger.Error("Failed to setup EventSub", zap.Error(err))
client.Logger.Error("Failed to setup EventSub", "error", err)
}
tpl := client.GetTemplateEngine()
client.Chat = chat.Setup(ctx, db, userClient, client.User, logger, tpl)
client.Alerts = alerts.Setup(ctx, db, logger, tpl)
client.Timers = timers.Setup(ctx, db, logger)
client.Chat = chat.Setup(ctx, db, userClient, client.User, client.Logger, tpl)
client.Alerts = alerts.Setup(ctx, db, client.Logger, tpl)
client.Timers = timers.Setup(ctx, db, client.Logger)
}
} else {
client.Logger.Warn("Twitch user not identified, this will break most features")
@ -186,14 +184,14 @@ func (c *Client) runStatusPoll() {
UserIDs: []string{c.User.ID},
})
if err != nil {
c.Logger.Error("Error checking stream status", zap.Error(err))
c.Logger.Error("Error checking stream status", "error", err)
return
}
c.streamOnline.Set(len(status.Data.Streams) > 0)
err = c.DB.PutJSON(twitch.StreamInfoKey, status.Data.Streams)
if err != nil {
c.Logger.Warn("Error saving stream info", zap.Error(err))
c.Logger.Warn("Error saving stream info", "error", err)
}
}()

View File

@ -1,28 +1,26 @@
package client
import (
"context"
"testing"
"git.sr.ht/~ashkeel/strimertul/twitch"
"go.uber.org/zap/zaptest"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/webserver"
)
func TestNewClient(t *testing.T) {
logger := zaptest.NewLogger(t)
client, _ := database.CreateInMemoryLocalClient(t)
defer database.CleanupLocalClient(client)
server, err := webserver.NewServer(client, logger, webserver.DefaultServerFactory)
server, err := webserver.NewServer(client, webserver.DefaultServerFactory)
if err != nil {
t.Fatal(err)
}
config := twitch.Config{}
_, err = newClient(config, client, server, logger)
_, err = newClient(context.Background(), config, client, server)
if err != nil {
t.Fatal(err)
}

View File

@ -1,6 +1,7 @@
package client
import (
"log/slog"
"math/rand"
"strconv"
"strings"
@ -11,7 +12,6 @@ import (
"git.sr.ht/~ashkeel/strimertul/twitch/template"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
)
const ChatCounterPrefix = "twitch/chat/counters/"
@ -57,7 +57,7 @@ func (c *Client) GetTemplateEngine() template.Engine {
counter++
err := c.DB.PutKey(counterKey, strconv.Itoa(counter))
if err != nil {
c.Logger.Error("Error saving key", zap.Error(err), zap.String("key", counterKey))
c.Logger.Error("Error saving key", "error", err, slog.String("key", counterKey))
}
return counter
},

View File

@ -3,16 +3,15 @@ package eventsub
import (
"context"
"fmt"
"log/slog"
"time"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/utils"
"github.com/gorilla/websocket"
lru "github.com/hashicorp/golang-lru/v2"
jsoniter "github.com/json-iterator/go"
"github.com/nicklaw5/helix/v2"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/utils"
)
var json = jsoniter.ConfigFastest
@ -23,14 +22,14 @@ type Client struct {
ctx context.Context
twitchAPI *helix.Client
db database.Database
logger *zap.Logger
logger *slog.Logger
user helix.User
eventCache *lru.Cache[string, time.Time]
savedSubscriptions map[string]bool
}
func Setup(ctx context.Context, twitchAPI *helix.Client, user helix.User, db database.Database, logger *zap.Logger) (*Client, error) {
func Setup(ctx context.Context, twitchAPI *helix.Client, user helix.User, db database.Database, logger *slog.Logger) (*Client, error) {
eventCache, err := lru.New[string, time.Time](128)
if err != nil {
return nil, fmt.Errorf("could not create LRU cache for events: %w", err)
@ -58,11 +57,11 @@ func (c *Client) eventSubLoop() {
for endpoint != "" {
endpoint, connection, err = c.connectWebsocket(endpoint, connection)
if err != nil {
c.logger.Error("EventSub websocket read error", zap.Error(err))
c.logger.Error("EventSub websocket read error", "error", err)
}
}
if connection != nil {
utils.Close(connection, c.logger)
utils.Close(connection)
}
}
@ -86,7 +85,7 @@ func readLoop(connection *websocket.Conn, recv chan<- []byte, wsErr chan<- error
func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (string, *websocket.Conn, error) {
connection, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
c.logger.Error("Could not establish a connection to the EventSub websocket", zap.Error(err))
c.logger.Error("Could not establish a connection to the EventSub websocket", "error", err)
return "", nil, err
}
@ -109,7 +108,7 @@ func (c *Client) connectWebsocket(url string, oldConnection *websocket.Conn) (st
var wsMessage WebsocketMessage
err = json.Unmarshal(messageData, &wsMessage)
if err != nil {
c.logger.Error("Error decoding EventSub message", zap.Error(err))
c.logger.Error("Error decoding EventSub message", "error", err)
continue
}
@ -128,30 +127,30 @@ func (c *Client) processMessage(wsMessage WebsocketMessage, oldConnection *webso
var welcomeData WelcomeMessagePayload
err := json.Unmarshal(wsMessage.Payload, &welcomeData)
if err != nil {
c.logger.Error("Error decoding EventSub welcome message", zap.String("message-type", wsMessage.Metadata.MessageType), zap.Error(err))
c.logger.Error("Error decoding EventSub welcome message", slog.String("message-type", wsMessage.Metadata.MessageType), "error", err)
break
}
c.logger.Info("Connection to EventSub websocket established", zap.String("session-id", welcomeData.Session.ID))
c.logger.Info("Connection to EventSub websocket established", slog.String("session-id", welcomeData.Session.ID))
// We can only close the old connection once the new one has been established
if oldConnection != nil {
utils.Close(oldConnection, c.logger)
utils.Close(oldConnection)
}
// Add subscription to websocket session
err = c.addSubscriptionsForSession(welcomeData.Session.ID)
if err != nil {
c.logger.Error("Could not add subscriptions", zap.Error(err))
c.logger.Error("Could not add subscriptions", "error", err)
break
}
case "session_reconnect":
var reconnectData WelcomeMessagePayload
err := json.Unmarshal(wsMessage.Payload, &reconnectData)
if err != nil {
c.logger.Error("Error decoding EventSub session reconnect parameters", zap.String("message-type", wsMessage.Metadata.MessageType), zap.Error(err))
c.logger.Error("Error decoding EventSub session reconnect parameters", slog.String("message-type", wsMessage.Metadata.MessageType), "error", err)
break
}
c.logger.Info("EventSub websocket requested a reconnection", zap.String("session-id", reconnectData.Session.ID), zap.String("reconnect-url", reconnectData.Session.ReconnectURL))
c.logger.Info("EventSub websocket requested a reconnection", slog.String("session-id", reconnectData.Session.ID), slog.String("reconnect-url", reconnectData.Session.ReconnectURL))
return reconnectData.Session.ReconnectURL, true, nil
case "notification":
@ -166,7 +165,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
// Check if we processed this already
if message.Metadata.MessageID != "" {
if c.eventCache.Contains(message.Metadata.MessageID) {
c.logger.Debug("Received duplicate event, ignoring", zap.String("message-id", message.Metadata.MessageID))
c.logger.Debug("Received duplicate event, ignoring", slog.String("message-id", message.Metadata.MessageID))
return
}
}
@ -176,7 +175,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
var notificationData NotificationMessagePayload
err := json.Unmarshal(message.Payload, &notificationData)
if err != nil {
c.logger.Error("Error decoding EventSub notification payload", zap.String("message-type", message.Metadata.MessageType), zap.Error(err))
c.logger.Error("Error decoding EventSub notification payload", slog.String("message-type", message.Metadata.MessageType), "error", err)
}
notificationData.Date = time.Now()
@ -184,7 +183,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
historyKey := fmt.Sprintf("%s%s", HistoryKeyPrefix, notificationData.Subscription.Type)
err = c.db.PutJSON(eventKey, notificationData)
if err != nil {
c.logger.Error("Error storing event to database", zap.String("key", eventKey), zap.Error(err))
c.logger.Error("Error storing event to database", slog.String("key", eventKey), "error", err)
}
var archive []NotificationMessagePayload
@ -198,7 +197,7 @@ func (c *Client) processEvent(message WebsocketMessage) {
}
err = c.db.PutJSON(historyKey, archive)
if err != nil {
c.logger.Error("Error storing event to database", zap.String("key", historyKey), zap.Error(err))
c.logger.Error("Error storing event to database", slog.String("key", historyKey), "error", err)
}
}
@ -221,7 +220,7 @@ func (c *Client) addSubscriptionsForSession(session string) error {
Condition: topicCondition(topic, c.user.ID),
})
if sub.Error != "" || sub.ErrorMessage != "" {
c.logger.Error("EventSub Subscription error", zap.String("topic", topic), zap.String("topic-version", version), zap.String("err", sub.Error), zap.String("message", sub.ErrorMessage))
c.logger.Error("EventSub Subscription error", slog.String("topic", topic), slog.String("topic-version", version), slog.String("err", sub.Error), slog.String("message", sub.ErrorMessage))
return fmt.Errorf("%s: %s", sub.Error, sub.ErrorMessage)
}
if err != nil {

View File

@ -2,15 +2,14 @@ package timers
import (
"context"
"log/slog"
"math/rand"
"time"
"git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go"
"go.uber.org/zap"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
jsoniter "github.com/json-iterator/go"
)
var json = jsoniter.ConfigFastest
@ -23,12 +22,12 @@ type Module struct {
lastTrigger *sync.Map[string, time.Time]
messages *sync.Slice[int]
logger *zap.Logger
logger *slog.Logger
db database.Database
ctx context.Context
}
func Setup(ctx context.Context, db database.Database, logger *zap.Logger) *Module {
func Setup(ctx context.Context, db database.Database, logger *slog.Logger) *Module {
mod := &Module{
lastTrigger: sync.NewMap[string, time.Time](),
messages: sync.NewSlice[int](),
@ -45,28 +44,28 @@ func Setup(ctx context.Context, db database.Database, logger *zap.Logger) *Modul
// Load config from database
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
logger.Debug("Config load error", zap.Error(err))
logger.Debug("Config load error", "error", err)
mod.Config = Config{
Timers: make(map[string]ChatTimer),
}
// Save empty config
err = db.PutJSON(ConfigKey, mod.Config)
if err != nil {
logger.Warn("Could not save default config for bot timers", zap.Error(err))
logger.Warn("Could not save default config for bot timers", "error", err)
}
}
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
if err := json.UnmarshalFromString(value, &mod.Config); err != nil {
logger.Warn("Error reloading timer config", zap.Error(err))
logger.Warn("Error reloading timer config", "error", err)
return
}
logger.Info("Reloaded timer config")
}); err != nil {
logger.Error("Could not set-up timer reload subscription", zap.Error(err))
logger.Error("Could not set-up timer reload subscription", "error", err)
}
logger.Debug("Loaded timers", zap.Int("timers", len(mod.Config.Timers)))
logger.Debug("Loaded timers", slog.Int("timers", len(mod.Config.Timers)))
// Start goroutine for clearing message counters and running timers
go mod.runTimers()
@ -84,7 +83,7 @@ func (m *Module) runTimers() {
err := m.db.PutJSON(chat.ActivityKey, m.messages.Get())
if err != nil {
m.logger.Warn("Error saving chat activity", zap.Error(err))
m.logger.Warn("Error saving chat activity", "error", err)
}
// Calculate activity

View File

@ -1,7 +0,0 @@
package utils
type ContextKey string
const (
ContextLogger ContextKey = "logger"
)

View File

@ -2,15 +2,14 @@ package utils
import (
"io"
"log/slog"
"reflect"
"runtime/debug"
"go.uber.org/zap"
)
func Close(res io.Closer, logger *zap.Logger) {
func Close(res io.Closer) {
err := res.Close()
if err != nil {
logger.Error("Could not close resource", zap.String("name", reflect.TypeOf(res).String()), zap.Error(err), zap.String("stack", string(debug.Stack())))
slog.Error("Could not close resource", slog.String("name", reflect.TypeOf(res).String()), slog.String("stack", string(debug.Stack())), "error", err)
}
}

View File

@ -1,15 +1,14 @@
package utils
import (
"git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go"
)
"encoding/json"
var json = jsoniter.ConfigFastest
"git.sr.ht/~ashkeel/containers/sync"
)
func LoadJSONToWrapped[T any](data string, sync sync.Wrapped[T]) error {
var result T
err := json.UnmarshalFromString(data, &result)
err := json.Unmarshal([]byte(data), &result)
if err != nil {
return err
}

View File

@ -4,28 +4,24 @@ import (
"context"
crand "crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/fs"
"log/slog"
mrand "math/rand"
"net/http"
"net/http/pprof"
"time"
"git.sr.ht/~ashkeel/containers/sync"
jsoniter "github.com/json-iterator/go"
kv "github.com/strimertul/kilovolt/v11"
"go.uber.org/zap"
kv "git.sr.ht/~ashkeel/kilovolt/v12"
"git.sr.ht/~ashkeel/strimertul/database"
)
var json = jsoniter.ConfigFastest
type WebServer struct {
Config *sync.RWSync[ServerConfig]
db database.Database
logger *zap.Logger
server Server
frontend fs.FS
hub *kv.Hub
@ -36,9 +32,8 @@ type WebServer struct {
factory ServerFactory
}
func NewServer(db database.Database, logger *zap.Logger, serverFactory ServerFactory) (*WebServer, error) {
func NewServer(db database.Database, serverFactory ServerFactory) (*WebServer, error) {
server := &WebServer{
logger: logger,
db: db,
server: nil,
requestedRoutes: sync.NewMap[string, http.Handler](),
@ -51,7 +46,7 @@ func NewServer(db database.Database, logger *zap.Logger, serverFactory ServerFac
err := db.GetJSON(ServerConfigKey, &config)
if err != nil {
if !errors.Is(err, database.ErrEmptyKey) {
logger.Warn("HTTP config is corrupted or could not be read", zap.Error(err))
slog.Warn("HTTP config is corrupted or could not be read", "error", err)
}
// Initialize with default config
server.Config.Set(ServerConfig{
@ -124,7 +119,7 @@ func (s *WebServer) makeMux() *http.ServeMux {
}
if s.hub != nil {
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
kv.ServeWs(s.hub, w, r)
s.hub.CreateWebsocketClient(w, r, kv.ClientOptions{})
})
}
config := s.Config.Get()
@ -160,7 +155,7 @@ func (s *WebServer) Listen() error {
for {
// Read config and make http request mux
config := s.Config.Get()
s.logger.Info("Starting HTTP server", zap.String("bind", config.Bind))
slog.Info("Starting HTTP server", slog.String("bind", config.Bind))
s.mux = s.makeMux()
// Make HTTP server instance
@ -172,25 +167,25 @@ func (s *WebServer) Listen() error {
}
// Start HTTP server
s.logger.Info("HTTP server started", zap.String("bind", config.Bind))
slog.Info("HTTP server started", slog.String("bind", config.Bind))
err = s.server.Start()
// If the server died, we need to see what to do
s.logger.Debug("HTTP server died", zap.Error(err))
slog.Debug("HTTP server died", "error", err)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
exit <- err
return
}
// Are we trying to close or restart?
s.logger.Debug("HTTP server stopped", zap.Bool("restart", s.restart.Get()))
slog.Debug("HTTP server stopped", slog.Bool("restart", s.restart.Get()))
if s.restart.Get() {
s.restart.Set(false)
continue
}
break
}
s.logger.Debug("HTTP server stalled")
slog.Debug("HTTP server stalled")
exit <- nil
}()
@ -203,7 +198,7 @@ func (s *WebServer) onConfigUpdate(value string) {
var config ServerConfig
err := json.Unmarshal([]byte(value), &config)
if err != nil {
s.logger.Error("Failed to unmarshal config", zap.Error(err))
slog.Error("Failed to unmarshal config", "error", err)
return
}
@ -220,7 +215,7 @@ func (s *WebServer) onConfigUpdate(value string) {
s.restart.Set(true)
err = s.server.Shutdown(context.Background())
if err != nil {
s.logger.Error("Failed to shutdown server", zap.Error(err))
slog.Error("Failed to shutdown server", "error", err)
return
}
}

View File

@ -8,42 +8,37 @@ import (
"time"
"git.sr.ht/~ashkeel/containers/sync"
"go.uber.org/zap/zaptest"
"git.sr.ht/~ashkeel/strimertul/database"
)
func TestNewServer(t *testing.T) {
logger := zaptest.NewLogger(t)
client, _ := database.CreateInMemoryLocalClient(t)
defer database.CleanupLocalClient(client)
_, err := NewServer(client, logger, DefaultServerFactory)
_, err := NewServer(client, DefaultServerFactory)
if err != nil {
t.Fatal(err)
}
}
func TestNewServerWithTestFactory(t *testing.T) {
logger := zaptest.NewLogger(t)
client, _ := database.CreateInMemoryLocalClient(t)
defer database.CleanupLocalClient(client)
testServer := NewTestServer()
_, err := NewServer(client, logger, testServer.Factory())
_, err := NewServer(client, testServer.Factory())
if err != nil {
t.Fatal(err)
}
}
func TestListen(t *testing.T) {
logger := zaptest.NewLogger(t)
client, _ := database.CreateInMemoryLocalClient(t)
defer database.CleanupLocalClient(client)
// Create a test server
testServer := NewTestServer()
server, err := NewServer(client, logger, testServer.Factory())
server, err := NewServer(client, testServer.Factory())
if err != nil {
t.Fatal(err)
}
@ -96,12 +91,11 @@ func (t *testCustomHandler) ServeHTTP(http.ResponseWriter, *http.Request) {
}
func TestCustomRoute(t *testing.T) {
logger := zaptest.NewLogger(t)
client, _ := database.CreateInMemoryLocalClient(t)
defer database.CleanupLocalClient(client)
// Create test server
server, err := NewServer(client, logger, nil)
server, err := NewServer(client, nil)
if err != nil {
t.Fatal(err)
}