Skip to content

Commit 6f24dd9

Browse files
authored
feat: structured logging (#26)
* replace all logs with slogs * add request url to logger * log every request * changelog log config structure * use slog built in text unmarshaler * defer slog * replace log with slog * fix typo * rename var
1 parent d689546 commit 6f24dd9

File tree

12 files changed

+201
-28
lines changed

12 files changed

+201
-28
lines changed

cmd/server.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"flag"
66
"fmt"
77
"log"
8+
"log/slog"
89
"net/http"
910
"os"
1011

@@ -14,6 +15,7 @@ import (
1415
"github.com/jonashiltl/openchangelog/internal/handler/rss"
1516
"github.com/jonashiltl/openchangelog/internal/handler/web"
1617
"github.com/jonashiltl/openchangelog/internal/handler/web/admin"
18+
"github.com/jonashiltl/openchangelog/internal/lgr"
1719
"github.com/jonashiltl/openchangelog/internal/store"
1820
"github.com/naveensrinivasan/httpcache"
1921
"github.com/naveensrinivasan/httpcache/diskcache"
@@ -25,19 +27,21 @@ import (
2527
func main() {
2628
cfg, err := parseConfig()
2729
if err != nil {
28-
fmt.Fprintf(os.Stderr, "Failed to read config: %s\n", err)
30+
slog.Error("failed to read config", lgr.ErrAttr(err))
2931
os.Exit(1)
3032
}
33+
slog.SetDefault(lgr.NewLogger(cfg))
34+
3135
mux := http.NewServeMux()
3236
cache, err := createCache(cfg)
3337
if err != nil {
34-
fmt.Fprintf(os.Stderr, "Failed to create cache: %s\n", err)
38+
slog.Error("failed to create cache", lgr.ErrAttr(err))
3539
os.Exit(1)
3640
}
3741

3842
st, err := createStore(cfg)
3943
if err != nil {
40-
fmt.Fprintf(os.Stderr, "Failed to create store: %s\n", err)
44+
slog.Error("failed to create store", lgr.ErrAttr(err))
4145
os.Exit(1)
4246
}
4347

@@ -50,7 +54,7 @@ func main() {
5054
rss.RegisterRSSHandler(mux, rss.NewEnv(cfg, loader))
5155
handler := cors.Default().Handler(mux)
5256

53-
fmt.Printf("Starting server at http://%s\n", cfg.Addr)
57+
slog.Info("Ready to serve requests", slog.String("addr", fmt.Sprintf("http://%s", cfg.Addr)))
5458
log.Fatal(http.ListenAndServe(cfg.Addr, handler))
5559
}
5660

@@ -62,10 +66,10 @@ func parseConfig() (config.Config, error) {
6266

6367
func createStore(cfg config.Config) (store.Store, error) {
6468
if cfg.IsDBMode() {
65-
log.Println("Starting Openchangelog backed by sqlite")
69+
slog.Info("Starting Openchangelog backed by sqlite")
6670
return store.NewSQLiteStore(cfg.SqliteURL)
6771
} else {
68-
log.Println("Starting Openchangelog in config mode")
72+
slog.Info("Starting Openchangelog in config mode")
6973
return store.NewConfigStore(cfg), nil
7074
}
7175
}
@@ -74,13 +78,13 @@ func createCache(cfg config.Config) (httpcache.Cache, error) {
7478
if cfg.Cache != nil {
7579
switch cfg.Cache.Type {
7680
case config.Memory:
77-
log.Println("using memory cache")
81+
slog.Info("using memory cache")
7882
return httpcache.NewMemoryCache(), nil
7983
case config.Disk:
8084
if cfg.Cache.Disk == nil {
8185
return nil, errors.New("missing 'cache.file' config section")
8286
}
83-
log.Println("using disk cache")
87+
slog.Info("using disk cache")
8488
return diskcache.NewWithDiskv(diskv.New(diskv.Options{
8589
BasePath: cfg.Cache.Disk.Location,
8690
CacheSizeMax: cfg.Cache.Disk.MaxSize, // bytes
@@ -89,7 +93,7 @@ func createCache(cfg config.Config) (httpcache.Cache, error) {
8993
if cfg.Cache.S3 == nil {
9094
return nil, errors.New("missing 'cache.s3' config section")
9195
}
92-
log.Println("using s3 cache")
96+
slog.Info("using s3 cache")
9397
return s3cache.New(cfg.Cache.S3.Bucket), nil
9498
}
9599
}

internal/analytics/tinybird/tinybird.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package tinybird
33
import (
44
"bytes"
55
"fmt"
6-
"log"
6+
"log/slog"
77
"net/http"
88
"sync"
99
"time"
1010

1111
"github.com/jonashiltl/openchangelog/internal/analytics"
12+
"github.com/jonashiltl/openchangelog/internal/lgr"
1213
"github.com/olivere/ndjson"
1314
)
1415

@@ -89,7 +90,7 @@ func (b *bird) sendBatch(events []analytics.Event) error {
8990

9091
req, err := http.NewRequest("POST", url, &buf)
9192
if err != nil {
92-
log.Printf("failed create new analytics request to tinybird: %s\n", err)
93+
slog.Error("failed create new analytics request to tinybird", lgr.ErrAttr(err))
9394
return err
9495
}
9596

@@ -98,13 +99,13 @@ func (b *bird) sendBatch(events []analytics.Event) error {
9899

99100
resp, err := b.client.Do(req)
100101
if err != nil {
101-
log.Printf("failed to send events to tinybird: %s\n", err)
102+
slog.Error("failed to send events to tinybird", lgr.ErrAttr(err))
102103
return err
103104
}
104105
defer resp.Body.Close()
105106

106107
if resp.StatusCode > http.StatusAccepted {
107-
log.Printf("received error status from tinybird: %s", resp.Status)
108+
slog.Error("received error status from tinybird", slog.String("status", resp.Status))
108109
return fmt.Errorf("received error status from tinybird: %s", resp.Status)
109110
}
110111

internal/config/config.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package config
22

33
import (
4+
"log/slog"
5+
46
"github.com/spf13/viper"
57
)
68

@@ -83,6 +85,19 @@ type AdminConfig struct {
8385
PasswordHash string `mapstructure:"passwordHash"`
8486
}
8587

88+
type LogConfig struct {
89+
Level LogLevel `mapstructure:"level"`
90+
Style string `mapstructure:"style"`
91+
}
92+
93+
type LogLevel string
94+
95+
func (l LogLevel) ToSlog() slog.Level {
96+
var sl slog.Level
97+
sl.UnmarshalText([]byte(l))
98+
return sl
99+
}
100+
86101
type Config struct {
87102
Addr string `mapstructure:"addr"`
88103
SqliteURL string `mapstructure:"sqliteUrl"`
@@ -92,6 +107,7 @@ type Config struct {
92107
Cache *CacheConfig `mapstructure:"cache"`
93108
Analytics *AnalyticsConfig `mapstructure:"analytics"`
94109
Admin *AdminConfig `mapstructure:"admin"`
110+
Log *LogConfig `mapstructure:"log"`
95111
}
96112

97113
func (c Config) HasGithubAuth() bool {

internal/handler/rest/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/jonashiltl/openchangelog/internal/errs"
9+
"github.com/jonashiltl/openchangelog/internal/lgr"
910
"github.com/jonashiltl/openchangelog/internal/store"
1011
)
1112

@@ -29,6 +30,7 @@ func bearerAuth(e *env, r *http.Request) (Token, error) {
2930
if err != nil {
3031
return Token{}, err
3132
}
33+
lgr.AddWorkspaceID(r, id.String())
3234
return Token{
3335
Key: key,
3436
WorkspaceID: id,

internal/handler/rest/routes.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/jonashiltl/openchangelog/internal/changelog"
99
"github.com/jonashiltl/openchangelog/internal/errs"
10+
"github.com/jonashiltl/openchangelog/internal/lgr"
1011
"github.com/jonashiltl/openchangelog/internal/store"
1112
)
1213

@@ -50,8 +51,8 @@ type env struct {
5051
loader *changelog.Loader
5152
}
5253

53-
func serveHTTP(env *env, h func(e *env, w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
54-
return func(w http.ResponseWriter, r *http.Request) {
54+
func serveHTTP(env *env, h func(e *env, w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
55+
return lgr.AttachLogger(func(w http.ResponseWriter, r *http.Request) {
5556
err := h(env, w, r)
5657

5758
if err != nil {
@@ -74,6 +75,8 @@ func serveHTTP(env *env, h func(e *env, w http.ResponseWriter, r *http.Request)
7475
if err != nil {
7576
http.Error(w, err.Error(), http.StatusInternalServerError)
7677
}
78+
79+
lgr.LogRequest(r.Context(), status, msg)
7780
}
78-
}
81+
})
7982
}

internal/handler/rss/routes.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/jonashiltl/openchangelog/internal/changelog"
99
"github.com/jonashiltl/openchangelog/internal/config"
1010
"github.com/jonashiltl/openchangelog/internal/errs"
11+
"github.com/jonashiltl/openchangelog/internal/lgr"
1112
)
1213

1314
type env struct {
@@ -26,8 +27,8 @@ func RegisterRSSHandler(mux *http.ServeMux, e *env) {
2627
mux.HandleFunc("GET /feed", serveHTTP(e, feedHandler))
2728
}
2829

29-
func serveHTTP(env *env, h func(e *env, w http.ResponseWriter, r *http.Request) error) func(http.ResponseWriter, *http.Request) {
30-
return func(w http.ResponseWriter, r *http.Request) {
30+
func serveHTTP(env *env, h func(e *env, w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
31+
return lgr.AttachLogger(func(w http.ResponseWriter, r *http.Request) {
3132
err := h(env, w, r)
3233
if err != nil {
3334
status := http.StatusInternalServerError
@@ -64,6 +65,8 @@ func serveHTTP(env *env, h func(e *env, w http.ResponseWriter, r *http.Request)
6465
if err != nil {
6566
http.Error(w, err.Error(), http.StatusInternalServerError)
6667
}
68+
69+
lgr.LogRequest(r.Context(), status, msg)
6770
}
68-
}
71+
})
6972
}

internal/handler/web/admin/routes.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package admin
22

33
import (
44
"errors"
5-
"log"
5+
"log/slog"
66
"net/http"
77

88
"github.com/jonashiltl/openchangelog/internal/config"
@@ -15,7 +15,7 @@ func RegisterAdminHandler(mux *http.ServeMux, e *env) {
1515
return
1616
}
1717

18-
log.Println("admin view is enabled at /admin")
18+
slog.Info("admin view is enabled at /admin")
1919
mux.HandleFunc("GET /admin", serveHTTP(e, adminOverview))
2020
mux.HandleFunc("GET /admin/{wid}", serveHTTP(e, details))
2121
}

internal/handler/web/index.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package web
22

33
import (
44
"errors"
5-
"log"
5+
"log/slog"
66
"net/http"
77

88
"github.com/jonashiltl/openchangelog/components"
@@ -31,7 +31,7 @@ func index(e *env, w http.ResponseWriter, r *http.Request) error {
3131
}
3232
err = ensurePasswordProvided(r, parsed.CL.PasswordHash)
3333
if err != nil {
34-
log.Printf("Blocked access to protected changelog: %s\n", parsed.CL.ID)
34+
slog.InfoContext(r.Context(), "blocked access to changelog", slog.String("changelog", parsed.CL.ID.String()))
3535

3636
go e.getAnalyticsEmitter(parsed.CL).Emit(analytics.NewAccessDeniedEvent(r, parsed.CL))
3737
return views.PasswordProtection(views.PasswordProtectionArgs{

internal/handler/web/routes.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package web
22

33
import (
44
"errors"
5-
"log"
65
"net/http"
76

87
"github.com/jonashiltl/openchangelog/internal/analytics"
@@ -12,7 +11,9 @@ import (
1211
"github.com/jonashiltl/openchangelog/internal/errs"
1312
"github.com/jonashiltl/openchangelog/internal/handler/web/static"
1413
"github.com/jonashiltl/openchangelog/internal/handler/web/views"
14+
"github.com/jonashiltl/openchangelog/internal/lgr"
1515
"github.com/jonashiltl/openchangelog/internal/store"
16+
"golang.org/x/exp/slog"
1617
)
1718

1819
func RegisterWebHandler(mux *http.ServeMux, e *env) {
@@ -62,7 +63,7 @@ func createEmitter(cfg config.Config) analytics.Emitter {
6263
switch cfg.Analytics.Provider {
6364
case config.Tinybird:
6465
if cfg.Analytics.Tinybird == nil {
65-
log.Println("Tinybird analytics is enabled, but the 'analytics.tinybird' config section is missing")
66+
slog.Warn("Tinybird analytics is enabled, but the 'analytics.tinybird' config section is missing")
6667
return analytics.NewNoopEmitter()
6768
}
6869
return tinybird.New(tinybird.TinybirdOptions{
@@ -73,8 +74,8 @@ func createEmitter(cfg config.Config) analytics.Emitter {
7374
return analytics.NewNoopEmitter()
7475
}
7576

76-
func serveHTTP(env *env, h func(e *env, w http.ResponseWriter, r *http.Request) error) func(http.ResponseWriter, *http.Request) {
77-
return func(w http.ResponseWriter, r *http.Request) {
77+
func serveHTTP(env *env, h func(e *env, w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
78+
return lgr.AttachLogger(func(w http.ResponseWriter, r *http.Request) {
7879
err := h(env, w, r)
7980
if err != nil {
8081
path := r.URL.Path
@@ -95,6 +96,8 @@ func serveHTTP(env *env, h func(e *env, w http.ResponseWriter, r *http.Request)
9596
args.Status = domErr.Status()
9697
}
9798

99+
defer lgr.LogRequest(r.Context(), args.Status, args.Message)
100+
98101
// if requesting widget, don't render html error, just error message
99102
if _, ok := r.URL.Query()["widget"]; ok {
100103
http.Error(w, args.Message, args.Status)
@@ -107,5 +110,5 @@ func serveHTTP(env *env, h func(e *env, w http.ResponseWriter, r *http.Request)
107110
http.Error(w, err.Error(), http.StatusInternalServerError)
108111
}
109112
}
110-
}
113+
})
111114
}

0 commit comments

Comments
 (0)