Added custom logging support using zerolog

This commit is contained in:
2025-02-09 17:16:48 +11:00
parent 68754991c8
commit b2ed646ffd
9 changed files with 222 additions and 25 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.env
.logs/
tmp/
projectreshoot
static/css/output.css

View File

@@ -16,7 +16,7 @@ dev:
test:
go mod tidy && \
go run . --port 3232 --test
go run . --port 3232 --test --loglevel trace
clean:
go clean

View File

@@ -2,7 +2,6 @@ package db
import (
"database/sql"
"fmt"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
@@ -27,7 +26,13 @@ func (user *User) SetPassword(conn *sql.DB, password string) error {
if err != nil {
return errors.Wrap(err, "conn.Exec")
}
fmt.Println(result)
ra, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "result.RowsAffected")
}
if ra != 1 {
return errors.New("Password was not updated")
}
return nil
}

4
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/joho/godotenv v1.5.1
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.33.0
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d
golang.org/x/crypto v0.33.0
)
@@ -14,5 +15,8 @@ require (
require (
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
golang.org/x/sys v0.30.0 // indirect
)

17
go.sum
View File

@@ -4,17 +4,34 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

80
logging/logger.go Normal file
View File

@@ -0,0 +1,80 @@
package logging
import (
"io"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
)
// Takes a log level as string and converts it to a zerolog.Level interface.
// If the string is not a valid input it will return zerolog.InfoLevel
func GetLogLevel(level string) zerolog.Level {
levels := map[string]zerolog.Level{
"trace": zerolog.TraceLevel,
"debug": zerolog.DebugLevel,
"info": zerolog.InfoLevel,
"warn": zerolog.WarnLevel,
"error": zerolog.ErrorLevel,
"fatal": zerolog.FatalLevel,
"panic": zerolog.PanicLevel,
}
logLevel, valid := levels[level]
if !valid {
return zerolog.InfoLevel
}
return logLevel
}
// Returns a pointer to a new log file with the specified path.
// Remember to call file.Close() when finished writing to the log file
func GetLogFile(path string) (*os.File, error) {
logPath := filepath.Join(path, "server.log")
file, err := os.OpenFile(
logPath,
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
0663,
)
if err != nil {
return nil, errors.Wrap(err, "os.OpenFile")
}
return file, nil
}
// Get a pointer to a new zerolog.Logger with the specified level and output
// Can provide a file, writer or both. Must provide at least one of the two
func GetLogger(
logLevel zerolog.Level,
w io.Writer,
logFile *os.File,
logDir string,
) (*zerolog.Logger, error) {
if w == nil && logFile == nil {
return nil, errors.New("No Writer provided for log output.")
}
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
var consoleWriter zerolog.ConsoleWriter
if w != nil {
consoleWriter = zerolog.ConsoleWriter{Out: w}
}
var output io.Writer
if logFile != nil {
if w != nil {
output = zerolog.MultiLevelWriter(logFile, consoleWriter)
} else {
output = logFile
}
} else {
output = consoleWriter
}
logger := zerolog.New(output).With().Timestamp().Caller().Logger().Level(logLevel)
return &logger, nil
}

54
main.go
View File

@@ -15,13 +15,14 @@ import (
"time"
"projectreshoot/db"
"projectreshoot/logging"
"projectreshoot/server"
"github.com/pkg/errors"
)
// Initializes and runs the server
func run(ctx context.Context, w io.Writer, args []string) error {
func run(ctx context.Context, w io.Writer, args map[string]string) error {
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()
@@ -30,10 +31,35 @@ func run(ctx context.Context, w io.Writer, args []string) error {
return errors.Wrap(err, "server.GetConfig")
}
var logfile *os.File = nil
if config.LogOutput == "both" || config.LogOutput == "file" {
logfile, err = logging.GetLogFile(config.LogDir)
if err != nil {
return errors.Wrap(err, "logging.GetLogFile")
}
defer logfile.Close()
}
var consoleWriter io.Writer
if config.LogOutput == "both" || config.LogOutput == "console" {
consoleWriter = w
}
logger, err := logging.GetLogger(
config.LogLevel,
consoleWriter,
logfile,
config.LogDir,
)
if err != nil {
return errors.Wrap(err, "logging.GetLogger")
}
conn, err := db.ConnectToDatabase(&config.TursoDBName, &config.TursoToken)
if err != nil {
return errors.Wrap(err, "db.ConnectToDatabase")
}
defer conn.Close()
srv := server.NewServer(config, conn)
httpServer := &http.Server{
@@ -41,9 +67,9 @@ func run(ctx context.Context, w io.Writer, args []string) error {
Handler: srv,
}
// TEST: runs function for testing in dev if --test flag true
if args[1] == "true" {
test(config, conn, httpServer)
// Runs function for testing in dev if --test flag true
if args["test"] == "true" {
test(config, logger, conn, httpServer)
return nil
}
@@ -77,10 +103,24 @@ var static embed.FS
// Start of runtime. Parse commandline arguments & flags, Initializes context
// and starts the server
func main() {
port := flag.String("port", "", "Override port")
test := flag.Bool("test", false, "Run test function")
// Parse commandline args
host := flag.String("host", "", "Override host to listen on")
port := flag.String("port", "", "Override port to listen on")
test := flag.Bool("test", false, "Run test function instead of main program")
loglevel := flag.String("loglevel", "", "Set log level")
logoutput := flag.String("logoutput", "", "Set log destination (file, console or both)")
flag.Parse()
args := []string{*port, strconv.FormatBool(*test)}
// Map the args for easy access
args := map[string]string{
"host": *host,
"port": *port,
"test": strconv.FormatBool(*test),
"loglevel": *loglevel,
"logoutput": *logoutput,
}
// Start the server
ctx := context.Background()
if err := run(ctx, os.Stdout, args); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)

View File

@@ -4,38 +4,78 @@ import (
"errors"
"fmt"
"os"
"projectreshoot/logging"
"github.com/joho/godotenv"
"github.com/rs/zerolog"
)
type Config struct {
Host string // Host to listen on
Port string // Port to listen on
TrustedHost string // Domain/Hostname to accept as trusted
TursoDBName string // DB Name for Turso DB/Branch
TursoToken string // Bearer token for Turso DB/Branch
SecretKey string // Secret key for signing tokens
AccessTokenExpiry int64 // Access token expiry in minutes
RefreshTokenExpiry int64 // Refresh token expiry in minutes
TokenFreshTime int64 // Time for tokens to stay fresh in minutes
Host string // Host to listen on
Port string // Port to listen on
TrustedHost string // Domain/Hostname to accept as trusted
TursoDBName string // DB Name for Turso DB/Branch
TursoToken string // Bearer token for Turso DB/Branch
SecretKey string // Secret key for signing tokens
AccessTokenExpiry int64 // Access token expiry in minutes
RefreshTokenExpiry int64 // Refresh token expiry in minutes
TokenFreshTime int64 // Time for tokens to stay fresh in minutes
LogLevel zerolog.Level // Log level for global logging. Defaults to info
LogOutput string // "file", "console", or "both". Defaults to console
LogDir string // Path to create log files
}
// Load the application configuration and get a pointer to the Config object
func GetConfig(args []string) (*Config, error) {
func GetConfig(args map[string]string) (*Config, error) {
err := godotenv.Load(".env")
if err != nil {
fmt.Println(".env file not found.")
}
var port string
var (
host string
port string
logLevel zerolog.Level
logOutput string
valid bool
)
if args[0] != "" {
port = args[0]
if args["host"] != "" {
host = args["host"]
} else {
host = GetEnvDefault("HOST", "127.0.0.1")
}
if args["port"] != "" {
port = args["port"]
} else {
port = GetEnvDefault("PORT", "3333")
}
if args["loglevel"] != "" {
logLevel = logging.GetLogLevel(args["loglevel"])
} else {
logLevel = logging.GetLogLevel(GetEnvDefault("LOG_LEVEL", "info"))
}
if args["logoutput"] != "" {
opts := map[string]string{
"both": "both",
"file": "file",
"console": "console",
}
logOutput, valid = opts[args["logoutput"]]
if !valid {
logOutput = "console"
fmt.Println(
"Log output type was not parsed correctly. Defaulting to console only",
)
}
} else {
logOutput = GetEnvDefault("LOG_OUTPUT", "console")
}
if logOutput != "both" && logOutput != "console" && logOutput != "file" {
logOutput = "console"
}
config := &Config{
Host: GetEnvDefault("HOST", "127.0.0.1"),
Host: host,
Port: port,
TrustedHost: os.Getenv("TRUSTED_HOST"),
TursoDBName: os.Getenv("TURSO_DB_NAME"),
@@ -44,6 +84,9 @@ func GetConfig(args []string) (*Config, error) {
AccessTokenExpiry: GetEnvInt64("ACCESS_TOKEN_EXPIRY", 5),
RefreshTokenExpiry: GetEnvInt64("REFRESH_TOKEN_EXPIRY", 1440), // defaults to 1 day
TokenFreshTime: GetEnvInt64("TOKEN_FRESH_TIME", 5),
LogLevel: logLevel,
LogOutput: logOutput,
LogDir: GetEnvDefault("LOG_DIR", ""),
}
if config.TrustedHost == "" {

View File

@@ -5,6 +5,8 @@ import (
"net/http"
"projectreshoot/server"
"github.com/rs/zerolog"
)
// This function will only be called if the --test commandline flag is set.
@@ -13,5 +15,10 @@ import (
// conflicts on the default 3333. Useful for testing things out during dev.
// If you add code here, remember to run:
// `git update-index --assume-unchanged tester.go` to avoid tracking changes
func test(config *server.Config, conn *sql.DB, srv *http.Server) {
func test(
config *server.Config,
logger *zerolog.Logger,
conn *sql.DB,
srv *http.Server,
) {
}