79b7f16c02c4f2cb7029f165f130ab4c25f0a8f7

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Use slog for logging

Diff

This diff is truncated to protect this page.

  1diff --git a/Dockerfile b/Dockerfile
  2index 9d71bb5b9685103eeace2f8d6b5eda9eadd38a11..d853d1154d8bcec5171ed248af29af7e0ba9c275 100644
  3--- a/Dockerfile
  4+++ b/Dockerfile
  5@@ -8,13 +8,16 @@ COPY . /app/
  6 RUN go build -o dist/ytrssil-api cmd/main.go
  7 
  8 FROM debian:trixie-slim AS api
  9+
 10+HEALTHCHECK --start-period=10s CMD exec curl -sf localhost:$PORT/healthz
 11+ENTRYPOINT ["/app/ytrssil-api"]
 12+
 13 RUN apt update \
 14 	&& apt install -y ca-certificates curl \
 15 	&& apt clean \
 16 	&& rm -rf /var/lib/apt/lists/*
 17 
 18 COPY --from=builder /app/dist/ /app/
 19-ENTRYPOINT ["/app/ytrssil-api"]
 20 
 21 FROM migrate/migrate AS migrations
 22 COPY ./migrations/ /migrations/
 23diff --git a/Makefile b/Makefile
 24index 41a274c7f9f238a37404c442fe79d1f68ce9a26d..5157b5ce5c2a3a9415d533d60d9f9ef1464d2c43 100644
 25--- a/Makefile
 26+++ b/Makefile
 27@@ -3,11 +3,11 @@
 28 DB_URI ?= postgres://ytrssil:ytrssil@localhost:5432/ytrssil?sslmode=disable
 29 
 30 bin/moq:
 31-	GOBIN=$(PWD)/bin go install github.com/matryer/moq@v0.5.3
 32+	GOBIN=$(PWD)/bin go install github.com/matryer/moq@v0.6.0
 33 bin/golangci-lint:
 34-	GOBIN=$(PWD)/bin go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.3.1
 35+	GOBIN=$(PWD)/bin go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
 36 bin/migrate: bin
 37-	GOBIN=$(PWD)/bin go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.18.3
 38+	GOBIN=$(PWD)/bin go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.19.0
 39 
 40 lint: bin/golangci-lint
 41 	go mod tidy
 42diff --git a/cmd/main.go b/cmd/main.go
 43index 37e62ab29f57a1854e74865fd53c32af9bc1e9e9..0a49766e1f711f9a014d05c14d79ee3821267641 100644
 44--- a/cmd/main.go
 45+++ b/cmd/main.go
 46@@ -3,6 +3,7 @@ package main
 47 import (
 48 	"context"
 49 	"fmt"
 50+	"log/slog"
 51 	"net/http"
 52 	"os"
 53 	"os/signal"
 54@@ -17,47 +18,58 @@ import (
 55 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/handler"
 56 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/auth"
 57 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/ytrssil"
 58-	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
 59 )
 60 
 61+const LevelFatal slog.Level = slog.LevelError + 4
 62+
 63 func init() {
 64 	// always use UTC
 65 	time.Local = time.UTC
 66 }
 67 
 68-func fetcherRoutine(l log.Logger, h handler.Handler) {
 69+func fetcherRoutine(l *slog.Logger, h handler.Handler) {
 70 	for {
 71 		err := h.FetchVideos(context.Background())
 72 		if err != nil {
 73-			l.Log("level", "ERROR", "function", "main.fetcherRoutine", "call", "handler.FetchVideos", "err", err)
 74+			l.Error("Failed to fetch videos", "call", "handler.FetchVideos", "err", err)
 75 		}
 76 		time.Sleep(5 * time.Minute)
 77 	}
 78 }
 79 
 80 func main() {
 81-	log := log.NewLogger()
 82+	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
 83 
 84 	config, err := config.Parse()
 85 	if err != nil {
 86-		log.Log("level", "FATAL", "call", "config.Parse", "error", err)
 87+		logger.Log(context.Background(), LevelFatal, "Failed to parse config", "call", "config.Parse", "error", err)
 88 		return
 89 	}
 90-	db, err := db.NewPostgresDB(log, config.DB)
 91+	db, err := db.NewPostgresDB(logger, config.DB)
 92 	if err != nil {
 93-		log.Log("level", "FATAL", "call", "db.NewPostgresDB", "error", err)
 94+		logger.Log(
 95+			context.Background(), LevelFatal,
 96+			"Failed to create DB connection",
 97+			"call", "db.NewPostgresDB",
 98+			"error", err,
 99+		)
100 		return
101 	}
102-	parser := feedparser.NewParser(log)
103-	handler := handler.New(log, db, parser)
104+	parser := feedparser.NewParser(logger)
105+	handler := handler.New(logger, db, parser)
106 	gin.SetMode(gin.ReleaseMode)
107 	router, err := ytrssil.SetupGinRouter(
108-		log,
109+		logger,
110 		handler,
111 		auth.AuthMiddleware(db),
112 	)
113 	if err != nil {
114-		log.Log("level", "FATAL", "call", "ytrssil.SetupGinServer", "error", err)
115+		logger.Log(
116+			context.Background(), LevelFatal,
117+			"Failed to set up gin server",
118+			"call", "ytrssil.SetupGinServer",
119+			"error", err,
120+		)
121 		return
122 	}
123 
124@@ -65,24 +77,16 @@ func main() {
125 		Addr:    fmt.Sprintf(":%v", config.Gin.Port),
126 		Handler: router,
127 	}
128-	server.RegisterOnShutdown(func() {
129-		log.Log("level", "INFO", "msg", "shutdown server.Close()")
130-	})
131-
132 	quit := make(chan os.Signal, 1)
133 	// handle Interrupt (ctrl-c) Term, used by `kill` et al, HUP which is commonly used to reload configs
134 	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
135 
136 	go func() {
137 		s := <-quit
138-		log.Log(
139-			"level", "INFO",
140-			"msg", "signalRecv, for quitting",
141-			"signal", s,
142-		)
143+		logger.Info("Received signal, shutting down", "signal", s)
144 		if err := server.Shutdown(context.Background()); err != nil {
145-			log.Log(
146diff --git a/cmd/main_test.go b/cmd/main_test.go
147index 98b8f0aa670d1032e09c4ae107f11ec35bb4b0c6..04f4f31ca58d38f3682aed461e9d6a16b0a32a59 100644
148--- a/cmd/main_test.go
149+++ b/cmd/main_test.go
150@@ -5,6 +5,8 @@ import (
151 	"context"
152 	"encoding/json"
153 	"fmt"
154+	"io"
155+	"log/slog"
156 	"net/http"
157 	"net/http/httptest"
158 	"testing"
159@@ -18,7 +20,6 @@ import (
160 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/handler"
161 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/auth"
162 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/ytrssil"
163-	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
164 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
165 )
166 
167@@ -29,7 +30,7 @@ func init() {
168 }
169 
170 func setupTestServer(t *testing.T) (*http.Server, db.DB) {
171-	l := log.NewNopLogger()
172+	l := slog.New(slog.NewTextHandler(io.Discard, nil))
173 
174 	db, err := db.NewPostgresDB(l, testConfig.DB)
175 	if !assert.NoError(t, err) {
176diff --git a/compose.yaml b/compose.yaml
177index 57f3d37cf0f472d2a17d44e49f0a69861d04e3e9..04a2cdda90178cdc837b889ba70fc98ff37d82d5 100644
178--- a/compose.yaml
179+++ b/compose.yaml
180@@ -28,11 +28,6 @@ services:
181       PORT: "80"
182     ports:
183       - "8080:80"
184-    healthcheck:
185-      test: "curl http://localhost:80/healthz"
186-      interval: 5s
187-      retries: 5
188-      timeout: 2s
189 
190   migrations:
191     image: theedgeofrage/ytrssil:migrations
192diff --git a/db/channels.go b/db/channels.go
193index 835e9773cf698f35a8b857629eecc3f757e90df9..4eafc1942cbdfbd7ec46c95f70a3e53ff7fa03d4 100644
194--- a/db/channels.go
195+++ b/db/channels.go
196@@ -11,7 +11,7 @@ var createChannelQuery = `INSERT INTO channels (id, name) VALUES ($1, $2) ON CON
197 func (d *postgresDB) CreateChannel(ctx context.Context, channel models.Channel) error {
198 	resp, err := d.db.ExecContext(ctx, createChannelQuery, channel.ID, channel.Name)
199 	if err != nil {
200-		d.l.Log("level", "ERROR", "function", "db.CreateChannel", "error", err)
201+		d.l.Error("Failed to create channel", "call", "sql.ExecContext", "error", err)
202 		return err
203 	}
204 	if affected, _ := resp.RowsAffected(); affected == 0 {
205@@ -26,7 +26,7 @@ var listChannelsQuery = `SELECT id, name FROM channels`
206 func (d *postgresDB) ListChannels(ctx context.Context) ([]models.Channel, error) {
207 	rows, err := d.db.QueryContext(ctx, listChannelsQuery)
208 	if err != nil {
209-		d.l.Log("level", "ERROR", "function", "db.ListChannels", "call", "sql.QueryContext", "error", err)
210+		d.l.Error("Failed to list channels", "call", "sql.QueryContext", "error", err)
211 		return nil, err
212 	}
213 	defer rows.Close()
214@@ -36,7 +36,7 @@ func (d *postgresDB) ListChannels(ctx context.Context) ([]models.Channel, error)
215 		var channel models.Channel
216 		err = rows.Scan(&channel.ID, &channel.Name)
217 		if err != nil {
218-			d.l.Log("level", "ERROR", "function", "db.ListChannels", "call", "sql.Scan", "error", err)
219+			d.l.Error("Failed to scan rows to list channels", "call", "sql.Scan", "error", err)
220 			return nil, err
221 		}
222 		channels = append(channels, channel)
223@@ -50,7 +50,7 @@ var getChannelSubscribersQuery = `SELECT username FROM user_subscriptions WHERE
224 func (d *postgresDB) GetChannelSubscribers(ctx context.Context, channelID string) ([]string, error) {
225 	rows, err := d.db.QueryContext(ctx, getChannelSubscribersQuery, channelID)
226 	if err != nil {
227-		d.l.Log("level", "ERROR", "function", "db.GetChannelSubscribers", "call", "sql.QueryContext", "error", err)
228+		d.l.Error("Filed to query channel subscribers", "call", "sql.QueryContext", "error", err)
229 		return nil, err
230 	}
231 	defer rows.Close()
232@@ -60,7 +60,7 @@ func (d *postgresDB) GetChannelSubscribers(ctx context.Context, channelID string
233 		var sub string
234 		err = rows.Scan(&sub)
235 		if err != nil {
236-			d.l.Log("level", "ERROR", "function", "db.GetChannelSubscribers", "call", "sql.Scan", "error", err)
237+			d.l.Error("Failed to scan channel subscribers", "call", "sql.Scan", "error", err)
238 			return nil, err
239 		}
240 		subs = append(subs, sub)
241@@ -80,7 +80,7 @@ ON CONFLICT DO NOTHING
242 func (d *postgresDB) SubscribeUserToChannel(ctx context.Context, username string, channelID string) error {
243 	resp, err := d.db.ExecContext(ctx, subscribeUserToChannelQuery, username, channelID)
244 	if err != nil {
245-		d.l.Log("level", "ERROR", "function", "db.SubscribeUserToChannel", "error", err)
246+		d.l.Error("Failed to subscribe to channel", "call", "sql.ExecContext", "error", err)
247 		return err
248 	}
249 	if affected, _ := resp.RowsAffected(); affected == 0 {
250@@ -95,7 +95,7 @@ var unsubscribeUserFromChannelQuery = `DELETE FROM user_subscriptions WHERE user
251 func (d *postgresDB) UnsubscribeUserFromChannel(ctx context.Context, username string, channelID string) error {
252 	resp, err := d.db.ExecContext(ctx, unsubscribeUserFromChannelQuery, username, channelID)
253 	if err != nil {
254-		d.l.Log("level", "ERROR", "function", "db.SubscribeUserToChannel", "error", err)
255+		d.l.Error("Failed to unsubscribe from channel", "call", "sql.ExecContext", "error", err)
256 		return err
257 	}
258 
259diff --git a/db/psql.go b/db/psql.go
260index 601740b0851563fa4ea2e6f0486042c672dbdd14..8695a3ceac434be9c167bf183aec9e2195b5e2f2 100644
261--- a/db/psql.go
262+++ b/db/psql.go
263@@ -2,19 +2,19 @@ package db
264 
265 import (
266 	"database/sql"
267+	"log/slog"
268 
269 	_ "github.com/lib/pq"
270 
271 	ytrssilConfig "gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/config"
272-	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
273 )
274 
275 type postgresDB struct {
276-	l  log.Logger
277+	l  *slog.Logger
278 	db *sql.DB
279 }
280 
281-func NewPostgresDB(log log.Logger, dbCfg ytrssilConfig.DB) (*postgresDB, error) {
282+func NewPostgresDB(log *slog.Logger, dbCfg ytrssilConfig.DB) (*postgresDB, error) {
283 	db, err := sql.Open("postgres", dbCfg.DBURI)
284 	if err != nil {
285 		return nil, err
286diff --git a/db/users.go b/db/users.go
287index c70cae8c0e174117d19bdd7104d834429f7bb652..8faae60a58febab79053662a7a58e900237463ee 100644
288--- a/db/users.go
289+++ b/db/users.go
290@@ -21,13 +21,13 @@ func (d *postgresDB) AuthenticateUser(ctx context.Context, user models.User) (bo
291 			return false, nil
292 		}
293 
294-		d.l.Log("level", "ERROR", "function", "db.AuthenticateUser", "error", err)
295+		d.l.Error("Failed to query user for auth", "error", err)
296 		return false, err
297 	}
298 
299 	match, err := argon2id.ComparePasswordAndHash(user.Password, hashedPassword)
300 	if err != nil {
301-		d.l.Log("level", "ERROR", "function", "db.AuthenticateUser", "error", err)
302+		d.l.Error("Failed to hash password", "error", err)
303 		return false, err
304 	}
305 
306@@ -39,7 +39,7 @@ var createUserQuery = `INSERT INTO users (username, password) VALUES ($1, $2) ON
307 func (d *postgresDB) CreateUser(ctx context.Context, user models.User) error {
308 	resp, err := d.db.ExecContext(ctx, createUserQuery, user.Username, user.Password)
309 	if err != nil {
310-		d.l.Log("level", "ERROR", "function", "db.CreateUser", "error", err)
311+		d.l.Error("Failed to create user", "error", err)
312 		return err
313 	}
314 
315@@ -55,7 +55,7 @@ var deleteUserQuery = `DELETE FROM users WHERE username = $1`
316 func (d *postgresDB) DeleteUser(ctx context.Context, username string) error {
317 	_, err := d.db.ExecContext(ctx, deleteUserQuery, username)
318 	if err != nil {
319-		d.l.Log("level", "ERROR", "function", "db.DeleteUser", "error", err)
320+		d.l.Error("Failed to delete user", "error", err)
321 		return err
322 	}
323 
324diff --git a/db/videos.go b/db/videos.go
325index ee8e82e611c0024ea7e9dffde77c7de1d3207ba2..05b5f6665c41b62d1e4436cc6d64c34e9ace2391 100644
326--- a/db/videos.go
327+++ b/db/videos.go
328@@ -27,7 +27,7 @@ var getNewVideosQuery = `
329 func (d *postgresDB) GetNewVideos(ctx context.Context, username string) ([]models.Video, error) {
330 	rows, err := d.db.QueryContext(ctx, getNewVideosQuery, username)
331 	if err != nil {
332-		d.l.Log("level", "ERROR", "function", "db.GetNewVideos", "call", "sql.QueryContext", "error", err)
333+		d.l.Error("Failed to query new videos", "call", "sql.QueryContext", "error", err)
334 		return nil, err
335 	}
336 	defer rows.Close()
337@@ -43,7 +43,7 @@ func (d *postgresDB) GetNewVideos(ctx context.Context, username string) ([]model
338 			&video.ChannelName,
339 		)
340 		if err != nil {
341-			d.l.Log("level", "ERROR", "function", "db.GetNewVideos", "call", "sql.Scan", "error", err)
342+			d.l.Error("Failed to scan rows for get new videos", "call", "sql.Scan", "error", err)
343 			return nil, err
344 		}
345 		videos = append(videos, video)
346@@ -72,7 +72,7 @@ var getWatchedVideosQuery = `
347 func (d *postgresDB) GetWatchedVideos(ctx context.Context, username string) ([]models.Video, error) {
348 	rows, err := d.db.QueryContext(ctx, getWatchedVideosQuery, username)
349 	if err != nil {
350-		d.l.Log("level", "ERROR", "function", "db.GetWatchedVideos", "call", "sql.QueryContext", "error", err)
351+		d.l.Error("Failed to query for watched videos", "call", "sql.QueryContext", "error", err)
352 		return nil, err
353 	}
354 	defer rows.Close()
355@@ -88,7 +88,7 @@ func (d *postgresDB) GetWatchedVideos(ctx context.Context, username string) ([]m
356 			&video.ChannelName,
357 		)
358 		if err != nil {
359-			d.l.Log("level", "ERROR", "function", "db.GetWatchedVideos", "call", "sql.Scan", "error", err)
360+			d.l.Error("Failed to scan rows for watched videos", "call", "sql.Scan", "error", err)
361 			return nil, err
362 		}
363 		videos = append(videos, video)
364@@ -110,7 +110,7 @@ ON CONFLICT DO NOTHING
365 func (d *postgresDB) AddVideo(ctx context.Context, video models.Video, channelID string) error {
366 	resp, err := d.db.ExecContext(ctx, addVideoQuery, video.ID, video.Title, video.PublishedTime, channelID)
367 	if err != nil {
368-		d.l.Log("level", "ERROR", "function", "db.AddVideo", "call", "sql.Exec", "error", err)
369+		d.l.Error("Failed to add video", "call", "sql.Exec", "error", err)
370 		return err
371 	}
372 	if affected, _ := resp.RowsAffected(); affected == 0 {
373@@ -125,7 +125,7 @@ var addVideoToUserQuery = `INSERT INTO user_videos (username, video_id) VALUES (
374 func (d *postgresDB) AddVideoToUser(ctx context.Context, username string, videoID string) error {
375 	_, err := d.db.ExecContext(ctx, addVideoToUserQuery, username, videoID)
376 	if err != nil {
377-		d.l.Log("level", "ERROR", "function", "db.AddVideoToUser", "error", err)
378+		d.l.Error("Failed to add video to user", "error", err)
379 		return err
380 	}
381 
382@@ -139,7 +139,7 @@ func (d *postgresDB) SetVideoWatchTime(
383 ) error {
384 	_, err := d.db.ExecContext(ctx, setVideoWatchTimeQuery, watchTime, username, videoID)
385 	if err != nil {
386-		d.l.Log("level", "ERROR", "function", "db.WatchVideo", "error", err)
387+		d.l.Error("", "error", err)
388 		return err
389 	}
390 
391diff --git a/feedparser/feedparser.go b/feedparser/feedparser.go
392index 2dbc4a5a3f52732542f90b9642968f8b5a740932..853304c59734a085c9ff4bf9d118955e60d87ee4 100644
393--- a/feedparser/feedparser.go
394+++ b/feedparser/feedparser.go
395@@ -5,12 +5,11 @@ import (
396 	"errors"
397 	"fmt"
398 	"io"
399+	"log/slog"
400 	"net/http"
401 	"sync"
402 
403 	"github.com/paulrosania/go-charset/charset"
404-
405-	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
406 )
407 
408 var (
409@@ -26,10 +25,10 @@ type Parser interface {
410 }
411 
412 type parser struct {
413-	log log.Logger
414+	log *slog.Logger
415 }
416 
417-func NewParser(l log.Logger) *parser {
418+func NewParser(l *slog.Logger) *parser {
419 	return &parser{
420 		log: l,
421 	}
422@@ -38,13 +37,13 @@ func NewParser(l log.Logger) *parser {
423 func (p *parser) fetch(url string) (io.ReadCloser, error) {
424 	req, err := http.NewRequest("GET", url, nil)
425 	if err != nil {
426-		p.log.Log("level", "ERROR", "function", "feedparser.fetch", "call", "http.NewRequest", "error", err)
427+		p.log.Error("Failed to create request", "call", "http.NewRequest", "error", err)
428 		return nil, err
429 	}
430 
431 	response, err := http.DefaultClient.Do(req)
432 	if err != nil {
433-		p.log.Log("level", "ERROR", "function", "feedparser.fetch", "call", "http.Do", "error", err)
434+		p.log.Error("Failed to fetch feed", "call", "http.Do", "error", err)
435 		return nil, err
436 	}
437 
438@@ -71,7 +70,7 @@ func (p *parser) Parse(channelID string) (*Channel, error) {
439 
440 	var channel Channel
441 	if err := xmlDecoder.Decode(&channel); err != nil {
442-		p.log.Log("level", "ERROR", "function", "feedparser.Parse", "call", "xml.Decode", "error", err)
443+		p.log.Error("Failed to decode XML for RSS feed", "call", "xml.Decode", "error", err)
444 		return nil, fmt.Errorf("%w: %s", ErrParseFailed, err.Error())
445 	}
446 	channel.ID = channelID
447diff --git a/go.mod b/go.mod
448index b71422033684c035c3a1ff5e40340d206c88443f..cebd7cb8ad113d0d975ad677790db6aeb51e3729 100644
449--- a/go.mod
450+++ b/go.mod
451@@ -1,46 +1,49 @@
452 module gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api
453 
454-go 1.19
455+go 1.23.0
456 
457 require (
458 	github.com/alexedwards/argon2id v1.0.0
459-	github.com/gin-gonic/gin v1.10.1
460-	github.com/go-kit/log v0.2.1
461+	github.com/gin-gonic/gin v1.11.0
462 	github.com/jessevdk/go-flags v1.6.1
463 	github.com/lib/pq v1.10.9
464 	github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c
465-	github.com/stretchr/testify v1.10.0
466+	github.com/stretchr/testify v1.11.1
467 )
468 
469 require (
470-	github.com/bytedance/sonic v1.11.6 // indirect
471-	github.com/bytedance/sonic/loader v0.1.1 // indirect
472-	github.com/cloudwego/base64x v0.1.4 // indirect
473-	github.com/cloudwego/iasm v0.2.0 // indirect
474+	github.com/bytedance/sonic v1.14.0 // indirect
475+	github.com/bytedance/sonic/loader v0.3.0 // indirect
476+	github.com/cloudwego/base64x v0.1.6 // indirect
477 	github.com/davecgh/go-spew v1.1.1 // indirect
478-	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
479-	github.com/gin-contrib/sse v0.1.0 // indirect
480-	github.com/go-logfmt/logfmt v0.5.1 // indirect
481+	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
482+	github.com/gin-contrib/sse v1.1.0 // indirect
483 	github.com/go-playground/locales v0.14.1 // indirect
484 	github.com/go-playground/universal-translator v0.18.1 // indirect
485-	github.com/go-playground/validator/v10 v10.20.0 // indirect
486+	github.com/go-playground/validator/v10 v10.27.0 // indirect
487 	github.com/goccy/go-json v0.10.2 // indirect
488-	github.com/google/go-cmp v0.5.8 // indirect
489+	github.com/goccy/go-yaml v1.18.0 // indirect
490 	github.com/json-iterator/go v1.1.12 // indirect
491-	github.com/klauspost/cpuid/v2 v2.2.7 // indirect
492+	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
493 	github.com/leodido/go-urn v1.4.0 // indirect
494 	github.com/mattn/go-isatty v0.0.20 // indirect
495 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
496 	github.com/modern-go/reflect2 v1.0.2 // indirect
497-	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
498+	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
499 	github.com/pmezard/go-difflib v1.0.0 // indirect
500+	github.com/quic-go/qpack v0.5.1 // indirect
501+	github.com/quic-go/quic-go v0.54.0 // indirect
502 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
503-	github.com/ugorji/go/codec v1.2.12 // indirect
504-	golang.org/x/arch v0.8.0 // indirect
505-	golang.org/x/crypto v0.23.0 // indirect
506-	golang.org/x/net v0.25.0 // indirect
507-	golang.org/x/sys v0.21.0 // indirect
508-	golang.org/x/text v0.15.0 // indirect
509-	google.golang.org/protobuf v1.34.1 // indirect
510+	github.com/ugorji/go/codec v1.3.0 // indirect
511+	go.uber.org/mock v0.5.0 // indirect
512+	golang.org/x/arch v0.20.0 // indirect
513+	golang.org/x/crypto v0.40.0 // indirect
514+	golang.org/x/mod v0.25.0 // indirect
515+	golang.org/x/net v0.42.0 // indirect
516+	golang.org/x/sync v0.16.0 // indirect
517+	golang.org/x/sys v0.35.0 // indirect
518+	golang.org/x/text v0.27.0 // indirect
519+	golang.org/x/tools v0.34.0 // indirect
520+	google.golang.org/protobuf v1.36.9 // indirect
521 	gopkg.in/yaml.v3 v3.0.1 // indirect
522 )
523diff --git a/go.sum b/go.sum
524index fd087c83db106d0209e1124a990c1af62efcb85d..c3639762fd4126bd74dfbf152345cd608530a08b 100644
525--- a/go.sum
526+++ b/go.sum
527@@ -1,46 +1,41 @@
528 github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
529 github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
530-github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
531-github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
532-github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
533-github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
534-github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
535-github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
536-github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
537-github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
538+github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
539+github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
540+github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
541+github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
542+github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
543+github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
544 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
545 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
546 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
547-github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
548-github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
549-github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
550-github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
551-github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
552-github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
553-github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
554-github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
555-github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
556-github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
557+github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
558+github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
559+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
560+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
561+github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
562+github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
563 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
564+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
565 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
566 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
567 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
568 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
569-github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
570-github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
571+github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
572+github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
573 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
574 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
575-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
576-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
577+github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
578+github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
579+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
580+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
581 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
582 github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
583 github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
584 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
585 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
586-github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
587-github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
588-github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
589-github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
590+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
591+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
592 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
593 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
594 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
595@@ -54,48 +49,53 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
596 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
597 github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c h1:P6XGcuPTigoHf4TSu+3D/7QOQ1MbL6alNwrGhcW7sKw=
598 github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c/go.mod h1:YnNlZP7l4MhyGQ4CBRwv6ohZTPrUJJZtEv4ZgADkbs4=
599-github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
600-github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
601+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
602+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
603 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
604 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
605+github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
606+github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
607+github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
608+github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
609 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
610 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
611 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
612-github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
613 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
614-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
615 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
616 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
617 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
618-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
619-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
620-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
621-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
622+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
623+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
624 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
625 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
626-github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
627diff --git a/handler/handler.go b/handler/handler.go
628index 7a0a0413d302e4cbab2647e23e95d2868aabde9f..7f1a22de4e57002b6741345957b59cb10f5fe3e8 100644
629--- a/handler/handler.go
630+++ b/handler/handler.go
631@@ -2,10 +2,10 @@ package handler
632 
633 import (
634 	"context"
635+	"log/slog"
636 
637 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/db"
638 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/feedparser"
639-	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
640 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
641 )
642 
643@@ -21,12 +21,12 @@ type Handler interface {
644 }
645 
646 type handler struct {
647-	log    log.Logger
648+	log    *slog.Logger
649 	db     db.DB
650 	parser feedparser.Parser
651 }
652 
653-func New(log log.Logger, db db.DB, parser feedparser.Parser) *handler {
654+func New(log *slog.Logger, db db.DB, parser feedparser.Parser) *handler {
655 	return &handler{
656 		log:    log,
657 		db:     db,
658diff --git a/handler/handler_test.go b/handler/handler_test.go
659index 53ef3613525e53821ce95ec95b742cac22c5f99f..902bc00157c42c16a57fc2ac1b7c0751f4a4637d 100644
660--- a/handler/handler_test.go
661+++ b/handler/handler_test.go
662@@ -2,13 +2,14 @@ package handler
663 
664 import (
665 	"context"
666+	"io"
667+	"log/slog"
668 	"testing"
669 	"time"
670 
671 	"github.com/stretchr/testify/assert"
672 
673 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/config"
674-	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
675 	db_mock "gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/mocks/db"
676 	parser_mock "gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/mocks/feedparser"
677 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
678@@ -21,9 +22,7 @@ func init() {
679 }
680 
681 func TestGetNewVideos(t *testing.T) {
682-	// Arrange
683-	l := log.NewNopLogger()
684-
685+	l := slog.New(slog.NewTextHandler(io.Discard, nil))
686 	handler := New(l, &db_mock.DBMock{
687 		GetNewVideosFunc: func(ctx context.Context, username string) ([]models.Video, error) {
688 			return []models.Video{
689@@ -37,11 +36,8 @@ func TestGetNewVideos(t *testing.T) {
690 			}, nil
691 		},
692 	}, &parser_mock.ParserMock{})
693-
694-	// Act
695 	resp, err := handler.GetNewVideos(context.TODO(), "username")
696 
697-	// Assert
698 	if assert.NoError(t, err) {
699 		if assert.NotNil(t, resp) {
700 			assert.Equal(t, resp[0].ID, "test")
701diff --git a/handler/videos.go b/handler/videos.go
702index 695b161040b5d68f1bab54127dcba4697ce5b7fe..8936945698a298115716b0f1299526d74f045387 100644
703--- a/handler/videos.go
704+++ b/handler/videos.go
705@@ -23,14 +23,14 @@ func (h *handler) GetWatchedVideos(ctx context.Context, username string) ([]mode
706 func (h *handler) addVideoToAllSubscribers(ctx context.Context, channelID string, videoID string) error {
707 	subs, err := h.db.GetChannelSubscribers(ctx, channelID)
708 	if err != nil {
709-		h.log.Log("level", "ERROR", "call", "db.GetChannelSubscribers", "err", err)
710+		h.log.Error("failed to get channel subscribers", "call", "db.GetChannelSubscribers", "err", err)
711 		return err
712 	}
713 
714 	for _, sub := range subs {
715 		err = h.db.AddVideoToUser(ctx, sub, videoID)
716 		if err != nil {
717-			h.log.Log("level", "ERROR", "call", "db.AddVideoToUser", "err", err)
718+			h.log.Error("Failed to add video to user", "call", "db.AddVideoToUser", "err", err)
719 			continue
720 		}
721 	}
722@@ -42,7 +42,7 @@ func (h *handler) addVideosForChannel(ctx context.Context, parsedChannel *feedpa
723 	for _, parsedVideo := range parsedChannel.Videos {
724 		date, err := parsedVideo.Published.Parse()
725 		if err != nil {
726-			h.log.Log("level", "WARNING", "call", "feedparser.Parse", "err", err)
727+			h.log.Warn("Failed to parse video information", "call", "feedparser.Parse", "err", err)
728 			continue
729 		}
730 
731@@ -55,7 +55,7 @@ func (h *handler) addVideosForChannel(ctx context.Context, parsedChannel *feedpa
732 		err = h.db.AddVideo(ctx, video, parsedChannel.ID)
733 		if err != nil {
734 			if !errors.Is(err, db.ErrVideoExists) {
735-				h.log.Log("level", "WARNING", "call", "db.AddVideo", "err", err)
736+				h.log.Warn("Failed to save video to db", "call", "db.AddVideo", "err", err)
737 			}
738 			continue
739 		}
740@@ -67,7 +67,7 @@ func (h *handler) addVideosForChannel(ctx context.Context, parsedChannel *feedpa
741 }
742 
743 func (h *handler) FetchVideos(ctx context.Context) error {
744-	h.log.Log("level", "INFO", "msg", "fetching new videos for all channels")
745+	h.log.Info("Fetching new videos for all channels")
746 
747 	channels, err := h.db.ListChannels(ctx)
748 	if err != nil {
749diff --git a/httpserver/ytrssil/api_setup_test.go b/httpserver/ytrssil/api_setup_test.go
750index 925a7e51865e5b4cb77ead943cabbd79ed077a3d..f4a230d1774d971960d52c7dad40f1e84c867373 100644
751--- a/httpserver/ytrssil/api_setup_test.go
752+++ b/httpserver/ytrssil/api_setup_test.go
753@@ -2,6 +2,8 @@ package ytrssil_test
754 
755 import (
756 	"fmt"
757+	"io"
758+	"log/slog"
759 	"net/http"
760 	"net/http/httptest"
761 	"testing"
762@@ -14,7 +16,6 @@ import (
763 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/handler"
764 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/auth"
765 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/ytrssil"
766-	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
767 )
768 
769 var testConfig config.Config
770@@ -26,7 +27,7 @@ func init() {
771 }
772 
773 func setupTestServer(t *testing.T) *http.Server {
774-	l := log.NewNopLogger()
775+	l := slog.New(slog.NewTextHandler(io.Discard, nil))
776 
777 	handler := handler.New(l, nil, nil)
778 
779diff --git a/httpserver/ytrssil/server.go b/httpserver/ytrssil/server.go
780index 548d4e985c29b2645bc2440310e6c418f8de3aa0..ebbe8222d06fa2ed414ab59aaa3ceb3fef7d8616 100644
781--- a/httpserver/ytrssil/server.go
782+++ b/httpserver/ytrssil/server.go
783@@ -1,33 +1,48 @@
784 package ytrssil
785 
786 import (
787+	"fmt"
788+	"log/slog"
789 	"net/http"
790+	"time"
791 
792 	"github.com/gin-gonic/gin"
793 
794 	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/handler"
795-	"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
796 )
797 
798 type server struct {
799-	log     log.Logger
800+	log     *slog.Logger
801 	handler handler.Handler
802 }
803 
804-func NewServer(log log.Logger, handler handler.Handler) (*server, error) {
805+func NewServer(log *slog.Logger, handler handler.Handler) (*server, error) {
806 	return &server{log: log, handler: handler}, nil
807 }
808 
809+func ginLogFormatter(param gin.LogFormatterParams) string {
810+	return fmt.Sprintf("timestamp=%v status=%d duration=%v size=%v ip=%s method=%s path=%#v error=%s\n",
811+		param.TimeStamp.UTC().Format(time.RFC3339Nano),
812+		param.StatusCode,
813+		param.Latency,
814+		param.BodySize,
815+		param.ClientIP,
816+		param.Method,
817+		param.Path,
818+		param.ErrorMessage,
819+	)
820+}
821+
822 func (s *server) Healthz(c *gin.Context) {
823 	c.String(http.StatusOK, "healthy")
824 }
825 
826 // SetupGinRouter sets up routes for all APIs on a Gin server (aka router)
827-func SetupGinRouter(l log.Logger, handler handler.Handler, authMiddleware func(c *gin.Context)) (*gin.Engine, error) {
828+func SetupGinRouter(l *slog.Logger, handler handler.Handler, authMiddleware func(c *gin.Context)) (*gin.Engine, error) {
829 	engine := gin.New()
830 	// Middlewares are executed top to bottom in a stack-like manner
831 	engine.Use(
832-		gin.LoggerWithFormatter(log.GinFormatterWithUTCAndBodySize),
833+		gin.LoggerWithFormatter(ginLogFormatter),
834 		gin.Recovery(), // Recovery needs to go before other middlewares to catch panics
835 	)
836 
837diff --git a/lib/log/log.go b/lib/log/log.go
838deleted file mode 100644
839index 8efd86e2d4b1fa079b8a5553dfad3649c799759a..0000000000000000000000000000000000000000
840--- a/lib/log/log.go
841+++ /dev/null
842@@ -1,60 +0,0 @@
843-package log
844-
845-import (
846-	"fmt"
847-	stdlog "log"
848-	"os"
849-
850-	"github.com/gin-gonic/gin"
851-	"github.com/go-kit/log"
852-)
853-
854-type Logger interface {
855-	Log(keyvals ...interface{}) error
856-}
857-
858-func NewLogger() Logger {
859-	var logger log.Logger
860-	logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
861-	stdlog.SetOutput(log.NewStdlibAdapter(logger))
862-	logger = log.With(logger, "ts", log.DefaultTimestampUTC)
863-	// We're wrapping gotkit, so DefaultCaller would show: log.go:23
864-	logger = log.With(logger, "caller", log.Caller(3))
865-
866-	return logger
867-}
868-
869-// NewNopLogger is go-kit/log.NewNopLogger
870-func NewNopLogger() log.Logger {
871-	return log.NewNopLogger()
872-}
873-
874-// NewSyncLogger is useful for debugging, use sparingly
875-func NewSyncLogger() log.Logger {
876-	return log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
877-}
878-
879-// Str is used to log unknown types using fmt
880-func Str(i any) string { return fmt.Sprintf("%+v", i) }
881-
882-// GinFormatterWithUTCAndBodySize is the default gin loggger with:
883-//   - UTC times
884-//   - logs the response size in bytes
885-func GinFormatterWithUTCAndBodySize(param gin.LogFormatterParams) string {
886-	var statusColor, methodColor, resetColor string
887-	if param.IsOutputColor() {
888-		statusColor = param.StatusCodeColor()
889-		methodColor = param.MethodColor()
890-		resetColor = param.ResetColor()
891-	}
892-	return fmt.Sprintf("[DUNE] %v |%s %3d %s| %13v | %6v bytes | %15s |%s %-7s %s %#v\n%s",
893-		param.TimeStamp.UTC().Format("2006/01/02 15:04:05.000"),
894-		statusColor, param.StatusCode, resetColor,
895-		param.Latency,
896-		param.BodySize,
897-		param.ClientIP,
898-		methodColor, param.Method, resetColor,
899-		param.Path,
900-		param.ErrorMessage,
901-	)
902-}