prepare switch to log/slog

This commit prepares the switch from log to log/slog, which was
introduced in Go 1.21.
slog provides some useful facilities to add metadata to log entries,
which should be helpful for debugging problems.

This commit also adds a small transaction ID generator. It provides a
common identifier between log messages, so that multiple errors can be
viewed together in their order.
This commit is contained in:
Gibheer 2023-08-17 22:02:52 +02:00
parent a4a8c64229
commit 10f7eb53f4
3 changed files with 109 additions and 2 deletions

View File

@ -6,8 +6,10 @@ import (
"flag"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"log/slog"
"net"
"net/http"
"os"
@ -49,6 +51,11 @@ type (
Mode string `toml:"mode"`
List []string `toml:"list"`
}
Log struct {
Format string `toml:"format"`
Level string `toml:"level"`
Output string `toml:"output"`
}
}
MapEntry struct {
@ -99,6 +106,8 @@ func main() {
log.Fatalf("could not parse config: %s", err)
}
logger := parseLogger(config)
db, err := sql.Open("postgres", config.DB)
if err != nil {
log.Fatalf("could not open database connection: %s", err)
@ -168,7 +177,7 @@ func main() {
l = tls.NewListener(l, tlsConf)
}
s := newServer(l, db, tmpl, auth, autho)
s := newServer(l, db, logger, tmpl, auth, autho)
s.Handle("/", showChecks)
s.Handle("/create", showCreate)
s.Handle("/check", showCheck)
@ -179,6 +188,48 @@ func main() {
log.Fatalf("http server stopped: %s", s.ListenAndServe())
}
func parseLogger(config Config) *slog.Logger {
var output io.Writer
switch config.Log.Output {
case "", "stderr":
output = os.Stderr
case "stdout":
output = os.Stdout
default:
var err error
output, err = os.OpenFile(config.Log.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0640)
if err != nil {
log.Fatalf("could not open log file handler: %s", err)
}
}
var level slog.Level
switch config.Log.Level {
case "debug":
level = slog.LevelDebug
case "", "info":
level = slog.LevelInfo
case "warn":
level = slog.LevelWarn
case "error":
level = slog.LevelError
default:
log.Fatalf("unknown log level '%s', only 'debug', 'info', 'warn' and 'error' are supported", config.Log.Level)
}
var handler slog.Handler
switch config.Log.Format {
case "", "text":
handler = slog.NewTextHandler(output, &slog.HandlerOptions{Level: level})
case "json":
handler = slog.NewJSONHandler(output, &slog.HandlerOptions{Level: level})
default:
log.Fatalf("unknown log format '%s', only 'text' and 'json' are supported", config.Log.Format)
}
return slog.New(handler)
}
func checkAction(con *Context) {
if con.r.Method != "POST" {
con.w.WriteHeader(http.StatusMethodNotAllowed)

View File

@ -2,12 +2,15 @@ package main
import (
"compress/gzip"
"crypto/rand"
"database/sql"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"log/slog"
"math/big"
"net"
"net/http"
"strings"
@ -19,6 +22,7 @@ type (
listen net.Listener
db *sql.DB
h *http.ServeMux
log *slog.Logger
tmpl *template.Template
auth func(c *Context) error // authentication
autho func(c *Context) error // authorization
@ -32,6 +36,7 @@ type (
r *http.Request
tmpl *template.Template
db *sql.DB
log *slog.Logger
User string `json:"-"`
Filter *filter `json:"-"`
@ -51,7 +56,7 @@ type (
}
)
func newServer(l net.Listener, db *sql.DB, tmpl *template.Template, auth func(c *Context) error, autho func(c *Context) error) *server {
func newServer(l net.Listener, db *sql.DB, log *slog.Logger, tmpl *template.Template, auth func(c *Context) error, autho func(c *Context) error) *server {
s := &server{
listen: l,
db: db,
@ -59,6 +64,7 @@ func newServer(l net.Listener, db *sql.DB, tmpl *template.Template, auth func(c
h: http.NewServeMux(),
auth: auth,
autho: autho,
log: log,
}
return s
}
@ -70,11 +76,19 @@ func (s *server) ListenAndServe() error {
func (s *server) Handle(path string, fun handleFunc) {
s.h.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
txid, err := newTxId()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`internal error occured`))
s.log.Error("could not create txid", "error", err)
return
}
c := &Context{
w: w,
r: r,
tmpl: s.tmpl,
db: s.db,
log: s.log.With("path", path, "txid", txid),
}
if err := s.auth(c); err != nil {
return
@ -153,3 +167,32 @@ func (c *Context) SetCookie(name, val string, expire time.Time) {
http.SetCookie(c.w, &cook)
return
}
// Alphabet are the available characters used to generate the txids. The more
// characters available the more bits can be represented with shorter IDs.
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_"
// alphalen is the number of available characters in the alphabet.
var alphalen = big.NewInt(int64(len(alphabet)))
// txlen is the length of the generated transaction IDs
const txlen = 15
// generate a new transaction ID
//
// This function generates a unique ID using the above parameters. If the IDs
// generated are not unique enough to track a transaction, the txlen should
// be raised.
func newTxId() (string, error) {
s := make([]byte, txlen)
var err error
var num *big.Int
for i := 0; i < txlen; i++ {
num, err = rand.Int(rand.Reader, alphalen)
if err != nil {
return "", err
}
s[i] = alphabet[num.Int64()]
}
return string(s), nil
}

View File

@ -64,3 +64,16 @@ mode = "all"
# The list defines the usernames allowed to change data in the frontend. They
# must be authenticated to get the permission.
#list = ["user1", "user2"]
[log]
# With format the log output format can be switched between `text`
# and `json` output.
#format = "text"
# With level the amount of logs can be reduced when necessary. The supported
# levels are `error`, `warn`, `info` and `debug`.
#level = "info"
# Output decides where to send all generated log output. It can either be a path
# or one of the special outputs `stdout` or `stderr`.
#output = "stderr"